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
582#[derive(Debug, Clone, PartialEq)]
583pub(crate) enum ComputedWrite {
584    Cell {
585        seq: u64,
586        sheet_id: SheetId,
587        row0: u32,
588        col0: u32,
589        value: OverlayValue,
590        format_id: Option<crate::format::FormatId>,
591    },
592    Rect {
593        seq: u64,
594        sheet_id: SheetId,
595        sr0: u32,
596        sc0: u32,
597        values: Vec<Vec<OverlayValue>>,
598    },
599    /// Consecutive rows `row0..` of one column (a family run's commit),
600    /// each with its format.
601    Run {
602        seq: u64,
603        sheet_id: SheetId,
604        row0: u32,
605        col0: u32,
606        entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
607    },
608}
609
610impl ComputedWrite {
611    #[inline]
612    pub(crate) fn seq(&self) -> u64 {
613        match self {
614            ComputedWrite::Cell { seq, .. }
615            | ComputedWrite::Rect { seq, .. }
616            | ComputedWrite::Run { seq, .. } => *seq,
617        }
618    }
619}
620
621#[derive(Debug, Default)]
622pub(crate) struct ComputedWriteBuffer {
623    writes: Vec<ComputedWrite>,
624    next_seq: u64,
625    estimated_bytes: usize,
626    formats_present: bool,
627}
628
629impl ComputedWriteBuffer {
630    const ENTRY_BASE_BYTES: usize = 32;
631
632    #[inline]
633    pub(crate) fn is_empty(&self) -> bool {
634        self.writes.is_empty()
635    }
636
637    #[inline]
638    pub(crate) fn len(&self) -> usize {
639        self.writes.len()
640    }
641
642    #[inline]
643    pub(crate) fn writes(&self) -> &[ComputedWrite] {
644        &self.writes
645    }
646
647    #[inline]
648    pub(crate) fn estimated_bytes(&self) -> usize {
649        self.estimated_bytes
650    }
651
652    #[cfg(feature = "tracing")]
653    fn traced_cell_count(&self) -> usize {
654        self.writes
655            .iter()
656            .map(|write| match write {
657                ComputedWrite::Cell { .. } => 1,
658                ComputedWrite::Rect { values, .. } => values.iter().map(Vec::len).sum(),
659                ComputedWrite::Run { entries, .. } => entries.len(),
660            })
661            .sum()
662    }
663
664    pub(crate) fn push_cell(
665        &mut self,
666        sheet_id: SheetId,
667        row0: u32,
668        col0: u32,
669        value: OverlayValue,
670    ) {
671        self.push_cell_with_format(sheet_id, row0, col0, value, None);
672    }
673
674    pub(crate) fn push_cell_with_format(
675        &mut self,
676        sheet_id: SheetId,
677        row0: u32,
678        col0: u32,
679        value: OverlayValue,
680        format_id: Option<crate::format::FormatId>,
681    ) {
682        let format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
683        self.formats_present |= format_id.is_some();
684        let seq = self.next_sequence();
685        self.estimated_bytes = self
686            .estimated_bytes
687            .saturating_add(Self::estimate_value_bytes(&value));
688        self.writes.push(ComputedWrite::Cell {
689            seq,
690            sheet_id,
691            row0,
692            col0,
693            value,
694            format_id,
695        });
696    }
697
698    pub(crate) fn push_column_run(
699        &mut self,
700        sheet_id: SheetId,
701        row0: u32,
702        col0: u32,
703        mut entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
704    ) {
705        let seq = self.next_sequence();
706        let mut added = 0usize;
707        for (value, format_id) in entries.iter_mut() {
708            *format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
709            self.formats_present |= format_id.is_some();
710            added = added.saturating_add(Self::estimate_value_bytes(value));
711        }
712        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
713        self.writes.push(ComputedWrite::Run {
714            seq,
715            sheet_id,
716            row0,
717            col0,
718            entries,
719        });
720    }
721
722    pub(crate) fn push_rect(
723        &mut self,
724        sheet_id: SheetId,
725        sr0: u32,
726        sc0: u32,
727        values: Vec<Vec<OverlayValue>>,
728    ) {
729        let seq = self.next_sequence();
730        let added = values
731            .iter()
732            .flat_map(|row| row.iter())
733            .map(Self::estimate_value_bytes)
734            .fold(0usize, usize::saturating_add);
735        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
736        self.writes.push(ComputedWrite::Rect {
737            seq,
738            sheet_id,
739            sr0,
740            sc0,
741            values,
742        });
743    }
744
745    pub(crate) fn clear(&mut self) {
746        self.writes.clear();
747        self.estimated_bytes = 0;
748        self.formats_present = false;
749    }
750
751    fn take_writes(&mut self) -> (Vec<ComputedWrite>, bool) {
752        self.estimated_bytes = 0;
753        let formats_present = std::mem::take(&mut self.formats_present);
754        (std::mem::take(&mut self.writes), formats_present)
755    }
756
757    fn next_sequence(&mut self) -> u64 {
758        let seq = self.next_seq;
759        self.next_seq = self.next_seq.wrapping_add(1);
760        seq
761    }
762
763    #[inline]
764    fn estimate_value_bytes(value: &OverlayValue) -> usize {
765        Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
766    }
767}
768
769#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
770struct ComputedWriteChunkKey {
771    sheet_id: SheetId,
772    col0: u32,
773    chunk_idx: usize,
774    chunk_start_row0: u32,
775}
776
777#[derive(Debug, Clone, PartialEq)]
778pub(crate) struct ComputedWriteChunkEntryPlan {
779    pub(crate) row_in_chunk: usize,
780    pub(crate) seq: u64,
781    pub(crate) value: OverlayValue,
782    pub(crate) format_id: Option<crate::format::FormatId>,
783}
784
785#[derive(Debug, Clone, PartialEq, Eq)]
786pub(crate) enum ComputedWriteChunkPlanShape {
787    Point,
788    SparseOffsets {
789        entries: usize,
790        span_len: usize,
791    },
792    DenseRange {
793        start: usize,
794        len: usize,
795    },
796    RunRange {
797        start: usize,
798        len: usize,
799        runs: usize,
800    },
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub(crate) enum ComputedWriteFormatClear {
805    Range { start: usize, end: usize },
806    Offsets(Vec<usize>),
807}
808
809#[derive(Debug, Clone, PartialEq)]
810pub(crate) enum ComputedWriteChunkFormatEffect {
811    NoFormatWork,
812    ClearStale(ComputedWriteFormatClear),
813    SetExplicit(Vec<(usize, Option<crate::format::FormatId>)>),
814}
815
816#[derive(Debug, Clone, PartialEq)]
817pub(crate) struct ComputedWriteChunkPlan {
818    pub(crate) sheet_id: SheetId,
819    pub(crate) col0: u32,
820    pub(crate) chunk_idx: usize,
821    pub(crate) chunk_start_row0: u32,
822    pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
823    pub(crate) shape: ComputedWriteChunkPlanShape,
824    pub(crate) format_effect: ComputedWriteChunkFormatEffect,
825}
826
827#[derive(Debug, Clone, Default, PartialEq)]
828pub(crate) struct ComputedWriteCoalescingPlan {
829    pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
830    pub(crate) input_cells: usize,
831    pub(crate) coalesced_cells: usize,
832    pub(crate) overwritten_cells: usize,
833}
834
835impl ComputedWriteCoalescingPlan {
836    #[inline]
837    pub(crate) fn is_empty(&self) -> bool {
838        self.chunks.is_empty()
839    }
840}
841
842impl ComputedWriteChunkPlan {
843    fn from_group(
844        key: ComputedWriteChunkKey,
845        mut entries: Vec<ComputedWriteChunkEntryPlan>,
846        formats_present: bool,
847        computed_lane_has_formats: bool,
848    ) -> (Self, usize) {
849        entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
850        let input_len = entries.len();
851        let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
852        for entry in entries {
853            if let Some(prev) = coalesced.last_mut()
854                && prev.row_in_chunk == entry.row_in_chunk
855            {
856                *prev = entry;
857                continue;
858            }
859            coalesced.push(entry);
860        }
861        let overwritten = input_len.saturating_sub(coalesced.len());
862        let shape = Self::classify_shape(&coalesced);
863        let format_effect = Self::classify_format_effect(
864            &coalesced,
865            &shape,
866            formats_present,
867            computed_lane_has_formats,
868        );
869        (
870            Self {
871                sheet_id: key.sheet_id,
872                col0: key.col0,
873                chunk_idx: key.chunk_idx,
874                chunk_start_row0: key.chunk_start_row0,
875                entries: coalesced,
876                shape,
877                format_effect,
878            },
879            overwritten,
880        )
881    }
882
883    fn classify_format_effect(
884        entries: &[ComputedWriteChunkEntryPlan],
885        shape: &ComputedWriteChunkPlanShape,
886        formats_present: bool,
887        computed_lane_has_formats: bool,
888    ) -> ComputedWriteChunkFormatEffect {
889        if !formats_present {
890            return if computed_lane_has_formats {
891                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
892            } else {
893                ComputedWriteChunkFormatEffect::NoFormatWork
894            };
895        }
896
897        if entries.iter().all(|entry| entry.format_id.is_none()) {
898            return if computed_lane_has_formats {
899                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
900            } else {
901                ComputedWriteChunkFormatEffect::NoFormatWork
902            };
903        }
904
905        ComputedWriteChunkFormatEffect::SetExplicit(
906            entries
907                .iter()
908                .map(|entry| (entry.row_in_chunk, entry.format_id))
909                .collect(),
910        )
911    }
912
913    fn format_clear_spec(
914        entries: &[ComputedWriteChunkEntryPlan],
915        shape: &ComputedWriteChunkPlanShape,
916    ) -> ComputedWriteFormatClear {
917        match shape {
918            ComputedWriteChunkPlanShape::DenseRange { start, len }
919            | ComputedWriteChunkPlanShape::RunRange { start, len, .. } => {
920                ComputedWriteFormatClear::Range {
921                    start: *start,
922                    end: start.saturating_add(*len),
923                }
924            }
925            ComputedWriteChunkPlanShape::Point
926            | ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
927                ComputedWriteFormatClear::Offsets(
928                    entries.iter().map(|entry| entry.row_in_chunk).collect(),
929                )
930            }
931        }
932    }
933
934    fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
935        debug_assert!(!entries.is_empty());
936        if entries.len() == 1 {
937            return ComputedWriteChunkPlanShape::Point;
938        }
939
940        let start = entries[0].row_in_chunk;
941        let end = entries[entries.len() - 1].row_in_chunk;
942        let span_len = end.saturating_sub(start).saturating_add(1);
943        if span_len != entries.len() {
944            return ComputedWriteChunkPlanShape::SparseOffsets {
945                entries: entries.len(),
946                span_len,
947            };
948        }
949
950        let runs = Self::run_count(entries);
951        if runs < entries.len() {
952            ComputedWriteChunkPlanShape::RunRange {
953                start,
954                len: entries.len(),
955                runs,
956            }
957        } else {
958            ComputedWriteChunkPlanShape::DenseRange {
959                start,
960                len: entries.len(),
961            }
962        }
963    }
964
965    fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
966        let mut runs = 0usize;
967        let mut prev: Option<&OverlayValue> = None;
968        for entry in entries {
969            if prev != Some(&entry.value) {
970                runs = runs.saturating_add(1);
971                prev = Some(&entry.value);
972            }
973        }
974        runs
975    }
976}
977
978#[cfg(feature = "tracing")]
979#[derive(Default)]
980struct TraceEvaluationCounters {
981    computed_vertices: usize,
982    cycles: usize,
983}
984
985pub struct Engine<R> {
986    pub(crate) graph: DependencyGraph,
987    resolver: R,
988    pub config: EvalConfig,
989    workbook_load_limits: crate::engine::WorkbookLoadLimits,
990    /// Clock for volatile date/time builtins, wrapped in a per-recalc
991    /// snapshot: sampled once at the start of every evaluation request
992    /// ([`Self::begin_evaluation_request`]) so all `NOW()`/`TODAY()` reads in
993    /// one recalc — including SCC iteration passes — agree (spec §7.11).
994    clock: crate::timezone::SnapshotClock,
995    thread_pool: Option<Arc<rayon::ThreadPool>>,
996    pub recalc_epoch: u64,
997    snapshot_id: std::sync::atomic::AtomicU64,
998    topology_epoch: u64,
999    cached_static_schedule: Option<CachedScheduleEntry>,
1000    /// Program 3 (plan reuse): schedules of recent earlier requests, most
1001    /// recent last (a user alternating between a few inputs recalculates
1002    /// the same few closures). Bounded by `RECENT_SCHEDULES` entries and
1003    /// `RECENT_SCHEDULE_VERTICES` candidate vertices in total.
1004    recent_schedules: Vec<CachedScheduleEntry>,
1005    /// Program 3 (plan reuse): the largest current schedule seen (usually
1006    /// the first evaluation's). A request it covers, and that is not much
1007    /// smaller, takes its schedule restricted to the request instead of
1008    /// planning (`Schedule::restrict`).
1009    base_schedule: Option<CachedScheduleEntry>,
1010    #[cfg(any(test, feature = "benchmark_internal"))]
1011    recalc_reuse_probe: std::sync::Mutex<RecalcReuseProbe>,
1012    spill_mgr: ShimSpillManager,
1013    /// Arrow-backed storage for sheet values (Phase A)
1014    arrow_sheets: SheetStore,
1015    /// Workbook-local number-format registry.
1016    format_registry: crate::format::FormatRegistry,
1017    /// Derived formula formats keyed by grid position, never graph vertex identity.
1018    derived_formats: crate::engine::derived_formats::DerivedFormats,
1019    #[cfg(test)]
1020    derived_format_operations_for_test: std::sync::atomic::AtomicU64,
1021    #[cfg(test)]
1022    family_members_for_test: std::sync::atomic::AtomicU64,
1023    #[cfg(test)]
1024    invariant_bound_members_for_test: std::sync::atomic::AtomicU64,
1025    #[cfg(test)]
1026    lifted_members_for_test: std::sync::atomic::AtomicU64,
1027    #[cfg(test)]
1028    chained_members_for_test: std::sync::atomic::AtomicU64,
1029    #[cfg(test)]
1030    lane_clean_reads_for_test: std::sync::atomic::AtomicU64,
1031    #[cfg(test)]
1032    criteria_kernel_members_for_test: std::sync::atomic::AtomicU64,
1033    #[cfg(test)]
1034    memo_hits_for_test: std::sync::atomic::AtomicU64,
1035    /// Authority build last compressed (`maybe_compress_formulas`).
1036    compressed_at_build: Option<u64>,
1037    #[cfg(test)]
1038    computed_overlay_set_explicit_entry_operations_for_test: u64,
1039    #[cfg(test)]
1040    computed_overlay_stale_clear_range_effects_for_test: u64,
1041    #[cfg(test)]
1042    computed_overlay_stale_clear_offset_attempts_for_test: u64,
1043    #[cfg(test)]
1044    computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64,
1045    /// True if any edit after bulk load; disables Arrow reads for parity
1046    has_edited: bool,
1047    /// Overlay compaction counter (Phase C instrumentation)
1048    overlay_compactions: u64,
1049
1050    // Overlay memory observability / budget (ticket 503)
1051    computed_overlay_bytes_estimate: usize,
1052    computed_overlay_mirroring_disabled: bool,
1053    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
1054    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
1055    pub(crate) force_materialize_range_views: bool,
1056    // Pass-scoped cache for Arrow used-row bounds per column
1057    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
1058    // Snapshot-scoped final used-axis bounds for open-ended references.
1059    used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
1060    lookup_index_cache: LookupIndexCache,
1061    source_cache: Arc<std::sync::RwLock<SourceCache>>,
1062    /// Identity binding for opaque source-family preparations.
1063    source_formula_token: Arc<()>,
1064    /// Dedicated identity binding for reusable recalculation plans.
1065    recalc_plan_token: Arc<()>,
1066    /// Staged formulas by sheet when `defer_graph_building` is enabled.
1067    staged_formulas: StagedFormulaMap,
1068    /// Presence and generation authority for ordinary staged formula discovery.
1069    staged_formula_index: StagedFormulaIndex,
1070    // Occupancy invalidation only: never a formula/read dependency.
1071    blocked_pending_spills: Vec<(VertexId, CellRef, Region)>,
1072    /// Per-sheet row visibility sidecar state.
1073    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
1074    /// Cached row visibility masks keyed by sheet/span/mode/version.
1075    row_visibility_mask_cache: std::sync::RwLock<
1076        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
1077    >,
1078    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
1079    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
1080    /// Last centralized formula ingest report.
1081    last_formula_ingest_report: Option<FormulaIngestReport>,
1082    /// Aggregate centralized formula ingest report for this engine.
1083    formula_ingest_report_total: FormulaIngestReport,
1084    /// Transient cancellation flag used during evaluation
1085    active_cancel_flag: Option<crate::engine::CancelToken>,
1086    /// Transient absolute deadline used by composed target and plan requests.
1087    active_evaluation_deadline: Option<Instant>,
1088
1089    /// Engine-level action depth.
1090    ///
1091    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
1092    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
1093    action_depth: u32,
1094
1095    // Phase 3b virtual-dependency convergence telemetry
1096    last_virtual_dep_telemetry: VirtualDepTelemetry,
1097    virtual_dep_fallback_activations: u64,
1098
1099    // Runtime-cycle SCC evaluation telemetry (RFC #112, Stage 2)
1100    last_cycle_telemetry: CycleTelemetry,
1101
1102    // C0 evaluation-resource observability. IDs are never reset or reused.
1103    next_evaluation_resource_request_id: u64,
1104    evaluation_resource_request_depth: usize,
1105    active_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1106    last_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1107    evaluation_resource_baseline: EvaluationResourceBaselineStats,
1108    evaluation_resource_request_started_at: Option<crate::instant::FzInstant>,
1109    evaluation_resource_budgets: crate::engine::EvaluationBudgets,
1110    evaluation_resource_config_diagnostic:
1111        Option<crate::engine::EvaluationResourceConfigDiagnostic>,
1112    active_resource_ledger: Option<ResourceLedger>,
1113    source_cache_footprints: Vec<std::sync::Weak<std::sync::atomic::AtomicU64>>,
1114    source_cache_accounted: u64,
1115
1116    /// SCC members that entered iterative calculation (`CyclePolicy::Iterate`
1117    /// with a witnessed live cycle) during the current evaluation request
1118    /// and must re-run on the next one.
1119    ///
1120    /// Excel re-evaluates circular cells on EVERY recalc (the accumulator
1121    /// contract, spec §4/§7.6), but this engine's dirty model marks SCC
1122    /// members clean after a recalc and would otherwise skip them forever.
1123    /// Resolution: members of iterating SCCs are redirtied volatile-like at
1124    /// the end of the same recalc that iterated them
1125    /// ([`Self::redirty_for_next_recalc`], called wherever
1126    /// `redirty_volatiles` runs). The set is per-recalc, never persisted:
1127    /// if an edit breaks the cycle, the next recalc's SCC task either does
1128    /// not exist or settles as phantom, nothing re-registers, and the
1129    /// redirty chain stops by itself.
1130    ///
1131    /// SCCs that landed on an exact fixed point are exempt: they go to
1132    /// [`Self::retained_scc_members`] instead and stay clean until the dirty
1133    /// graph (or a config change) reaches them (#368).
1134    pending_iterative_redirty: Vec<VertexId>,
1135    /// Members of iterating SCCs retained across recalcs (#368), keyed to the
1136    /// id of the retained SCC they belong to (ids come from
1137    /// `next_retained_scc_id`; grouping is only used for telemetry).
1138    ///
1139    /// An SCC is retained when the recalc that iterated it stopped because
1140    /// every member reproduced its previous value exactly (|Δ| = 0, or
1141    /// identity for non-numeric members; never NaN-converged), before the
1142    /// `max_iterations` cap, with no volatile or dynamic-reference member.
1143    /// Such an SCC is a fixed point of its own inputs: running it again with
1144    /// the same inputs cannot change any value, so it is not redirtied. The
1145    /// dirty graph remains the validity authority — any edit that reaches a
1146    /// member dirties it like any other formula and the SCC task re-runs.
1147    /// Membership is dropped when a member runs in an SCC task again (it is
1148    /// then re-retained or re-registered for per-recalc redirty), when the
1149    /// vertex is deleted, or when
1150    /// [`Self::reconcile_retained_sccs_at_request_begin`] invalidates the
1151    /// whole set because a config knob outside the graph changed.
1152    retained_scc_members: FxHashMap<VertexId, u64>,
1153    next_retained_scc_id: u64,
1154    /// [`Self::retained_scc_config_fingerprint`] as of the last retention.
1155    /// `Engine::config` is a public field, so knobs that change a retained
1156    /// SCC's result (cycle policy/tolerance, date system, determinism,
1157    /// volatile seeding) can change between recalcs without touching the
1158    /// graph; a mismatch at request begin dirties every retained member.
1159    /// Only meaningful while `retained_scc_members` is non-empty.
1160    retained_scc_config_fingerprint: u64,
1161    /// Function-registry semantic epoch and runtime-provider revision as of
1162    /// the last time retained SCCs were reconciled against them. A newer
1163    /// epoch dirties only the retained members whose formula calls a changed
1164    /// function (or every member when the change log is incomplete).
1165    retained_scc_function_epoch_seen: u64,
1166    retained_scc_provider_revision_seen: Option<u64>,
1167    /// Retained members that were already dirty when the current request
1168    /// began, with the SCC id they carried. A member still holding that id
1169    /// at the end of the request was not touched by any SCC task — its
1170    /// cycle dissolved (it evaluated as an ordinary formula) or the request
1171    /// never reached it — so [`Self::redirty_for_next_recalc`] drops it from
1172    /// the retained set instead of letting it linger.
1173    retained_scc_dirty_at_begin: Vec<(VertexId, u64)>,
1174
1175    /// Final committed values of iterating-SCC members (spec §4 persistence).
1176    /// In canonical (value-cache disabled) mode the computed overlay is the
1177    /// ONLY home of a formula's value, and structural edits clear computed
1178    /// overlays wholesale (`clear_computed_overlay_after_row/_col`) —
1179    /// destroying iteration state (accumulators reset to 0; found by the
1180    /// iterate edge corpus). This snapshot lets the next SCC task re-seed
1181    /// members whose overlay entry vanished. Members registered for
1182    /// per-recalc redirty are refreshed by
1183    /// [`Self::redirty_for_next_recalc`]; retained members are written once
1184    /// when retained. Entries are dropped when the member's SCC task ends
1185    /// without iterating or when the vertex is deleted. Empty unless
1186    /// something iterated — zero cost otherwise.
1187    iterative_state_values: FxHashMap<VertexId, LiteralValue>,
1188
1189    /// Global function-registry semantic epoch last observed.
1190    function_semantic_epoch_seen: u64,
1191    /// Runtime-provider semantic revision last observed.
1192    function_provider_revision_seen: Option<u64>,
1193
1194    #[cfg(feature = "tracing")]
1195    trace_evaluation_counters: TraceEvaluationCounters,
1196    #[cfg(test)]
1197    evaluation_request_begin_count_for_test: u64,
1198    #[cfg(any(test, feature = "test-support"))]
1199    before_prepared_span_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1200    #[cfg(test)]
1201    before_target_preparation_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1202    #[cfg(test)]
1203    before_target_planning_snapshot_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1204    #[cfg(test)]
1205    inject_target_semantic_stale_once_for_test: bool,
1206    #[cfg(test)]
1207    force_virtual_dep_changes_remaining_for_test: usize,
1208    /// Dynamic-read freshness state (design §8.2).
1209    freshness: freshness::Freshness,
1210    #[cfg(test)]
1211    fail_evaluation_commit_preflight_once_for_test: bool,
1212    #[cfg(test)]
1213    target_preparation_fault_for_test:
1214        Option<crate::engine::target_preparation::TargetPreparationFault>,
1215    #[cfg(test)]
1216    force_non_cycle_schedule_fallback_for_test: bool,
1217    #[cfg(test)]
1218    before_legacy_fallback_final_provider_sample_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1219    #[cfg(test)]
1220    after_eager_proposal_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1221}
1222
1223/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
1224/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
1225impl<R: EvaluationContext> Engine<R> {
1226    pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
1227        self.graph.ingest_pipeline(&self.resolver)
1228    }
1229}
1230
1231pub struct EngineAction<'a, R>
1232where
1233    R: EvaluationContext,
1234{
1235    engine: &'a mut Engine<R>,
1236    name: String,
1237    // Complete private mutation capture used by atomic actions.
1238    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
1239    capture: Option<*mut MutationCapture>,
1240    // Optional Arrow undo journal used by `Engine::action_atomic`.
1241    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
1242    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
1243    // True when this EngineAction must enforce conservative atomic transaction policy.
1244    atomic_policy: bool,
1245}
1246
1247impl<'a, R> EngineAction<'a, R>
1248where
1249    R: EvaluationContext,
1250{
1251    #[inline]
1252    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
1253        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1254        let coord = crate::reference::Coord::from_excel(row, col, true, true);
1255        crate::reference::CellRef::new(sheet_id, coord)
1256    }
1257
1258    #[inline]
1259    pub fn name(&self) -> &str {
1260        &self.name
1261    }
1262
1263    #[inline]
1264    pub fn set_cell_value(
1265        &mut self,
1266        sheet: &str,
1267        row: u32,
1268        col: u32,
1269        value: LiteralValue,
1270    ) -> Result<(), crate::engine::EditorError> {
1271        if self.capture.is_some() {
1272            let old_value = self.engine.read_cell_value(sheet, row, col);
1273            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1274            let addr = self.addr_for(sheet, row, col);
1275            let Some(capture_ptr) = self.capture else {
1276                return Err(crate::engine::EditorError::TransactionFailed {
1277                    reason: "action_with_logger: missing mutation capture".to_string(),
1278                });
1279            };
1280
1281            // For atomic journal mode, record computed overlay effects for this cell.
1282            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
1283            let old_comp = if self.arrow_undo.is_some() {
1284                self.engine.read_computed_overlay_cell(sheet, row, col)
1285            } else {
1286                None
1287            };
1288
1289            if self.engine.graph_admission_enabled() {
1290                let admission =
1291                    self.engine
1292                        .graph
1293                        .preview_value_mutation(addr.sheet_id, row, col)?;
1294                self.engine.preflight_graph_admission(admission)?;
1295            }
1296            if old_formula.is_none() {
1297                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1298            }
1299
1300            let delta_old_sem = if old_formula.is_some() {
1301                None
1302            } else {
1303                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1304            };
1305
1306            let start_len = unsafe { (&*capture_ptr).len() };
1307
1308            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1309            let capture = unsafe { &mut *capture_ptr };
1310            self.engine.edit_with_capture(capture, |editor| {
1311                editor.set_cell_value_with_old_state(
1312                    addr,
1313                    value.clone(),
1314                    old_value.clone(),
1315                    old_formula.clone(),
1316                );
1317            })?;
1318            self.engine.record_structural_change(StructuralScope::Cell {
1319                sheet: addr.sheet_id,
1320                row: addr.coord.row(),
1321                col: addr.coord.col(),
1322            });
1323
1324            if let Some(undo_ptr) = self.arrow_undo {
1325                // 1) Spill snapshot operations (computed overlay rect restore).
1326                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1327                let undo = unsafe { &mut *undo_ptr };
1328                self.engine
1329                    .record_spill_ops_into_arrow_undo(undo, new_events);
1330
1331                // 2) Delta/computed overlay single-cell deltas.
1332                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
1333                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1334                let row0 = row.saturating_sub(1);
1335                let col0 = col.saturating_sub(1);
1336                let delta_new_sem = Some(value.clone());
1337                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
1338                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
1339            }
1340            Ok(())
1341        } else {
1342            self.engine
1343                .set_cell_value(sheet, row, col, value)
1344                .map_err(crate::engine::EditorError::from)
1345        }
1346    }
1347
1348    #[inline]
1349    pub fn set_cell_formula(
1350        &mut self,
1351        sheet: &str,
1352        row: u32,
1353        col: u32,
1354        ast: ASTNode,
1355    ) -> Result<(), crate::engine::EditorError> {
1356        if self.capture.is_some() {
1357            let old_value = self.engine.read_cell_value(sheet, row, col);
1358            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1359            let addr = self.addr_for(sheet, row, col);
1360            let Some(capture_ptr) = self.capture else {
1361                return Err(crate::engine::EditorError::TransactionFailed {
1362                    reason: "action_with_logger: missing mutation capture".to_string(),
1363                });
1364            };
1365
1366            let admitted_formula = if self.engine.graph_admission_enabled() {
1367                let placement =
1368                    CellRef::new(addr.sheet_id, Coord::from_excel(row, col, true, true));
1369                let ingested = self.engine.ingest_pipeline().ingest_formula(
1370                    FormulaAstInput::Tree(ast.clone()),
1371                    placement,
1372                    None,
1373                )?;
1374                let admission = self.engine.graph.preview_formula_mutations(&[(
1375                    addr.sheet_id,
1376                    row,
1377                    col,
1378                    ingested.dep_plan.clone(),
1379                )])?;
1380                self.engine.preflight_graph_admission(admission)?;
1381                Some((ingested.ast_id, ingested.dep_plan))
1382            } else {
1383                None
1384            };
1385            if old_formula.is_none() {
1386                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1387            }
1388            let delta_old = if self.arrow_undo.is_some() {
1389                if old_formula.is_some() {
1390                    None
1391                } else {
1392                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1393                }
1394            } else {
1395                None
1396            };
1397            let start_len = unsafe { (&*capture_ptr).len() };
1398
1399            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1400            let capture = unsafe { &mut *capture_ptr };
1401            self.engine.edit_with_capture(capture, |editor| {
1402                if let Some((ast_id, plan)) = admitted_formula {
1403                    editor.set_cell_formula_with_prepared_plan(
1404                        addr,
1405                        ast.clone(),
1406                        old_value,
1407                        old_formula,
1408                        ast_id,
1409                        plan,
1410                    );
1411                } else {
1412                    editor.set_cell_formula_with_old_state(
1413                        addr,
1414                        ast.clone(),
1415                        old_value,
1416                        old_formula,
1417                    );
1418                }
1419            })?;
1420            self.engine.record_structural_change(StructuralScope::Cell {
1421                sheet: addr.sheet_id,
1422                row: addr.coord.row(),
1423                col: addr.coord.col(),
1424            });
1425
1426            if let Some(undo_ptr) = self.arrow_undo {
1427                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1428                let undo = unsafe { &mut *undo_ptr };
1429                self.engine
1430                    .record_spill_ops_into_arrow_undo(undo, new_events);
1431                let delta_new: Option<LiteralValue> = None;
1432                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1433                let row0 = row.saturating_sub(1);
1434                let col0 = col.saturating_sub(1);
1435                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
1436            }
1437            Ok(())
1438        } else {
1439            self.engine
1440                .set_cell_formula(sheet, row, col, ast)
1441                .map_err(crate::engine::EditorError::from)
1442        }
1443    }
1444
1445    #[inline]
1446    pub fn set_row_hidden(
1447        &mut self,
1448        sheet: &str,
1449        row_1based: u32,
1450        hidden: bool,
1451        source: RowVisibilitySource,
1452    ) -> Result<(), crate::engine::EditorError> {
1453        if self.capture.is_some() {
1454            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1455            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
1456            let old_hidden = self
1457                .engine
1458                .row_visibility
1459                .get(&sheet_id)
1460                .map(|state| state.is_row_hidden(row0, Some(source)))
1461                .unwrap_or(false);
1462            if old_hidden == hidden {
1463                return Ok(());
1464            }
1465
1466            let _ = self
1467                .engine
1468                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1469
1470            let Some(capture_ptr) = self.capture else {
1471                return Err(crate::engine::EditorError::TransactionFailed {
1472                    reason: "action_with_logger: missing mutation capture".to_string(),
1473                });
1474            };
1475            unsafe { &mut *capture_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
1476                sheet_id,
1477                row0,
1478                source,
1479                old_hidden,
1480                new_hidden: hidden,
1481            });
1482
1483            Ok(())
1484        } else {
1485            self.engine
1486                .set_row_hidden(sheet, row_1based, hidden, source)
1487        }
1488    }
1489
1490    #[inline]
1491    pub fn set_rows_hidden(
1492        &mut self,
1493        sheet: &str,
1494        start_row_1based: u32,
1495        end_row_1based: u32,
1496        hidden: bool,
1497        source: RowVisibilitySource,
1498    ) -> Result<(), crate::engine::EditorError> {
1499        if self.capture.is_some() {
1500            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1501            let (start_row0, end_row0) =
1502                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
1503
1504            let Some(capture_ptr) = self.capture else {
1505                return Err(crate::engine::EditorError::TransactionFailed {
1506                    reason: "action_with_logger: missing mutation capture".to_string(),
1507                });
1508            };
1509            let capture = unsafe { &mut *capture_ptr };
1510
1511            for row0 in start_row0..=end_row0 {
1512                let old_hidden = self
1513                    .engine
1514                    .row_visibility
1515                    .get(&sheet_id)
1516                    .map(|state| state.is_row_hidden(row0, Some(source)))
1517                    .unwrap_or(false);
1518                if old_hidden == hidden {
1519                    continue;
1520                }
1521
1522                let _ = self
1523                    .engine
1524                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1525
1526                capture.record(crate::engine::ChangeEvent::SetRowVisibility {
1527                    sheet_id,
1528                    row0,
1529                    source,
1530                    old_hidden,
1531                    new_hidden: hidden,
1532                });
1533            }
1534
1535            Ok(())
1536        } else {
1537            self.engine
1538                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
1539        }
1540    }
1541
1542    #[inline]
1543    pub fn insert_rows(
1544        &mut self,
1545        sheet: &str,
1546        before: u32,
1547        count: u32,
1548    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1549        if count == 0 {
1550            return Ok(crate::engine::ShiftSummary::default());
1551        }
1552        if self.capture.is_some() {
1553            let Some(capture_ptr) = self.capture else {
1554                return Err(crate::engine::EditorError::TransactionFailed {
1555                    reason: "action_atomic: missing mutation capture".to_string(),
1556                });
1557            };
1558
1559            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1560            let before0 = before.saturating_sub(1);
1561            let occupancy = self.engine.structural_row_occupancy(sheet, sheet_id);
1562            let affected_region = Engine::<R>::structural_row_region(sheet_id, before0);
1563
1564            // Graph structural insert (logged) - no snapshot bump.
1565            let summary = {
1566                let capture = unsafe { &mut *capture_ptr };
1567                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1568                    Ok(crate::engine::ShiftSummary::default());
1569                self.engine.edit_with_capture(capture, |editor| {
1570                    editor.set_structural_occupancy(occupancy);
1571                    out = editor.insert_rows(sheet_id, before0, count);
1572                })?;
1573                out?
1574            };
1575
1576            // Arrow insert (truth) + undo op.
1577            self.engine.ensure_arrow_sheet(sheet);
1578            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1579                asheet.insert_rows(before0 as usize, count as usize);
1580            }
1581            self.engine
1582                .purge_derived_formats_after_row(sheet_id, before0);
1583            self.engine
1584                .shift_row_visibility_insert(sheet_id, before0, count);
1585            self.engine.mark_moved_formula_vertices_dirty(&summary);
1586            self.engine
1587                .clear_computed_overlay_after_row(sheet, before0 as usize);
1588            self.engine
1589                .record_structural_change(StructuralScope::Region(affected_region));
1590            if let Some(undo_ptr) = self.arrow_undo {
1591                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
1592            }
1593            Ok(summary)
1594        } else {
1595            self.engine.insert_rows(sheet, before, count)
1596        }
1597    }
1598
1599    #[inline]
1600    pub fn delete_rows(
1601        &mut self,
1602        sheet: &str,
1603        start: u32,
1604        count: u32,
1605    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1606        if count == 0 {
1607            return Ok(crate::engine::ShiftSummary::default());
1608        }
1609        if self.atomic_policy {
1610            return Err(crate::engine::EditorError::TransactionUnsupported {
1611                reason:
1612                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
1613                        .to_string(),
1614            });
1615        }
1616        self.engine.delete_rows(sheet, start, count)
1617    }
1618
1619    #[inline]
1620    pub fn insert_columns(
1621        &mut self,
1622        sheet: &str,
1623        before: u32,
1624        count: u32,
1625    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1626        if count == 0 {
1627            return Ok(crate::engine::ShiftSummary::default());
1628        }
1629        if self.capture.is_some() {
1630            let Some(capture_ptr) = self.capture else {
1631                return Err(crate::engine::EditorError::TransactionFailed {
1632                    reason: "action_atomic: missing mutation capture".to_string(),
1633                });
1634            };
1635
1636            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1637            let before0 = before.saturating_sub(1);
1638            let occupancy = self.engine.structural_column_occupancy();
1639            let affected_region = Engine::<R>::structural_col_region(sheet_id, before0);
1640
1641            let summary = {
1642                let capture = unsafe { &mut *capture_ptr };
1643                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1644                    Ok(crate::engine::ShiftSummary::default());
1645                self.engine.edit_with_capture(capture, |editor| {
1646                    editor.set_structural_occupancy(occupancy);
1647                    out = editor.insert_columns(sheet_id, before0, count);
1648                })?;
1649                out?
1650            };
1651
1652            self.engine.ensure_arrow_sheet(sheet);
1653            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1654                asheet.insert_columns(before0 as usize, count as usize);
1655            }
1656            self.engine
1657                .purge_derived_formats_after_col(sheet_id, before0);
1658            self.engine.mark_moved_formula_vertices_dirty(&summary);
1659            self.engine
1660                .clear_computed_overlay_after_col(sheet, before0 as usize);
1661            self.engine
1662                .record_structural_change(StructuralScope::Region(affected_region));
1663            if let Some(undo_ptr) = self.arrow_undo {
1664                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
1665            }
1666            Ok(summary)
1667        } else {
1668            self.engine.insert_columns(sheet, before, count)
1669        }
1670    }
1671
1672    #[inline]
1673    pub fn delete_columns(
1674        &mut self,
1675        sheet: &str,
1676        start: u32,
1677        count: u32,
1678    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1679        if count == 0 {
1680            return Ok(crate::engine::ShiftSummary::default());
1681        }
1682        if self.atomic_policy {
1683            return Err(crate::engine::EditorError::TransactionUnsupported {
1684                reason:
1685                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
1686                        .to_string(),
1687            });
1688        }
1689        self.engine.delete_columns(sheet, start, count)
1690    }
1691
1692    /// Start an action from within an action.
1693    ///
1694    /// Nested actions are currently disallowed (ticket 614), so this will return a
1695    /// `EditorError::TransactionFailed` while an outer action is active.
1696    #[inline]
1697    pub fn action<T>(
1698        &mut self,
1699        name: impl AsRef<str>,
1700        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1701    ) -> Result<T, crate::engine::EditorError> {
1702        self.engine.action(name, f)
1703    }
1704}
1705
1706struct ActionDepthGuard<'a, R> {
1707    engine: *mut Engine<R>,
1708    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
1709}
1710
1711impl<'a, R> Drop for ActionDepthGuard<'a, R> {
1712    fn drop(&mut self) {
1713        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
1714        // than the surrounding `Engine::action` call.
1715        unsafe {
1716            let e = &mut *self.engine;
1717            e.action_depth = e.action_depth.saturating_sub(1);
1718        }
1719    }
1720}
1721
1722#[derive(Default)]
1723struct SourceCache {
1724    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
1725    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
1726}
1727
1728#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1729struct VisibilityMaskCacheKey {
1730    sheet_id: SheetId,
1731    start_row0: u32,
1732    end_row0: u32,
1733    mode: VisibilityMaskMode,
1734    version: u64,
1735}
1736
1737#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1738enum StructuralScope {
1739    Cell { sheet: SheetId, row: u32, col: u32 },
1740    Region(Region),
1741    Sheet(SheetId),
1742    RemovedSheet(SheetId),
1743    OpaqueGlobal,
1744    AllSheets,
1745}
1746
1747#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1748enum LoggedEditImpact {
1749    NoOp,
1750    DataOnly,
1751    Topology,
1752}
1753
1754#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1755enum LoggedEditDirection {
1756    Original,
1757    InverseReplay,
1758    ForwardReplay,
1759}
1760
1761#[derive(Clone, Copy)]
1762struct InvalidationBaseline {
1763    snapshot_id: u64,
1764    topology_epoch: u64,
1765}
1766
1767struct SourceCacheSession {
1768    cache: Arc<std::sync::RwLock<SourceCache>>,
1769}
1770
1771impl Drop for SourceCacheSession {
1772    fn drop(&mut self) {
1773        if let Ok(mut g) = self.cache.write() {
1774            *g = SourceCache::default();
1775        }
1776    }
1777}
1778
1779#[derive(Debug)]
1780#[non_exhaustive]
1781pub struct EvalResult {
1782    pub computed_vertices: usize,
1783    pub cycle_errors: usize,
1784    pub elapsed: std::time::Duration,
1785}
1786
1787#[derive(Clone, Debug, PartialEq, Eq)]
1788#[non_exhaustive]
1789pub struct TableMetadata {
1790    pub name: String,
1791    pub sheet: String,
1792    pub start_row: u32,
1793    pub start_col: u32,
1794    pub end_row: u32,
1795    pub end_col: u32,
1796    pub header_row: bool,
1797    pub headers: Vec<String>,
1798    pub totals_row: bool,
1799}
1800
1801/// Read-only engine counters used by benchmark/instrumentation tooling.
1802///
1803/// These counters are deliberately observational: collecting them must not mutate engine state or
1804/// alter formula evaluation semantics.
1805///
1806/// The `formula_plane_*` counters are always `0`: FormulaPlane spans were removed and the
1807/// dependency authority is the only runtime path. They are kept for source compatibility.
1808#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1809#[non_exhaustive]
1810pub struct EngineBaselineStats {
1811    pub graph_vertex_count: usize,
1812    pub graph_formula_vertex_count: usize,
1813    pub graph_edge_count: usize,
1814    pub dirty_vertex_count: usize,
1815    pub evaluation_vertex_count: usize,
1816    pub formula_ast_root_count: usize,
1817    pub formula_ast_node_count: usize,
1818    pub staged_formula_count: usize,
1819    pub formula_plane_active_span_count: usize,
1820    pub formula_plane_producer_result_entries: usize,
1821    pub formula_plane_consumer_read_entries: usize,
1822    pub formula_plane_mixed_topology_cache_builds: u64,
1823    pub formula_plane_mixed_topology_cache_hits: u64,
1824    pub formula_plane_mixed_topology_cache_overflows: u64,
1825    pub formula_plane_dirty_pending_events: usize,
1826    pub formula_plane_dirty_region_events_recorded: u64,
1827    pub formula_plane_dirty_span_region_events_recorded: u64,
1828    pub formula_plane_dirty_whole_span_seeds_recorded: u64,
1829    pub formula_plane_dirty_global_invalidations: u64,
1830    pub formula_plane_structural_span_candidates: u64,
1831    pub formula_plane_cycle_member_span_demotions: u64,
1832    pub formula_plane_array_result_span_demotions: u64,
1833    /// Members of exactly converged iterative SCCs currently retained across
1834    /// recalcs (#368).
1835    pub retained_scc_members: usize,
1836}
1837
1838#[derive(Debug, Clone, Default)]
1839#[non_exhaustive]
1840pub struct VirtualDepTelemetry {
1841    pub candidate_vertices_total: usize,
1842    pub vdeps_vertices_total: usize,
1843    pub vdeps_edges_total: usize,
1844    pub builder_elapsed_ms_total: u128,
1845    pub schedule_virtual_passes: usize,
1846    pub schedule_static_passes: usize,
1847    pub schedule_cache_hits: usize,
1848    pub schedule_cache_misses: usize,
1849    pub reused_schedule_vertices_total: usize,
1850    pub replan_iterations: usize,
1851    pub changed_vdeps_total: usize,
1852    pub bailout_reason: Option<&'static str>,
1853    pub fallback_mode_activations: u64,
1854}
1855
1856/// Per-recalc telemetry for SCC evaluation under `CycleDetection::Runtime`
1857/// (spec `formualizer-cycle-semantics-spec.md` §10).
1858///
1859/// Collection is unconditional: SCC tasks are rare relative to ordinary
1860/// vertex evaluation and the counters are a handful of integer adds per
1861/// task, so no config flag gates them (unlike [`VirtualDepTelemetry`],
1862/// which pays per-schedule costs). Counters reset at the start of every
1863/// evaluation request.
1864#[derive(Debug, Clone, Default, PartialEq)]
1865#[non_exhaustive]
1866pub struct CycleTelemetry {
1867    /// SCC tasks executed (static SCCs that reached Runtime evaluation).
1868    pub static_sccs: usize,
1869    /// SCC tasks whose live subgraph was acyclic — values produced.
1870    pub phantom_sccs: usize,
1871    /// Distinct live cycles witnessed across all SCC tasks.
1872    pub live_cycles_witnessed: usize,
1873    /// Cells stamped `#CIRC!` by Runtime SCC tasks.
1874    pub circ_cells_stamped: usize,
1875    /// Evaluation sweeps over (subsets of) SCC members, totalled across tasks
1876    /// (pass 1 included).
1877    pub settle_passes_total: usize,
1878    /// Largest pass count any single SCC task needed.
1879    pub max_passes_single_scc: usize,
1880    /// SCC tasks that entered iterative calculation (`CyclePolicy::Iterate`
1881    /// with a witnessed live cycle). RFC #113, Stage 3.
1882    pub iterated_sccs: usize,
1883    /// Iterating SCC tasks that stopped because every member passed the
1884    /// spec-§6 convergence test.
1885    pub converged_sccs: usize,
1886    /// SCC tasks that stopped at a pass cap. Under `CyclePolicy::Iterate`
1887    /// this is the Excel `max_iterations` cap (NOT an error — last values
1888    /// are kept; includes the no-convergence-test `max_iterations: 1`
1889    /// contract). Under `CyclePolicy::Error` it is the defensive acyclic
1890    /// settle cap (|SCC| + 2), which only a bug can hit.
1891    pub capped_sccs: usize,
1892    /// Largest `|Δ|` observed in any member's final-pass convergence
1893    /// comparison across iterating SCC tasks (numeric-class members only).
1894    /// `0.0` when no comparison ran (e.g. `max_iterations: 1`).
1895    pub max_abs_delta_at_stop: f64,
1896    /// Identical-bit NaN vs NaN member comparisons that were treated as
1897    /// converged (spec §6 NaN rule).
1898    pub nan_converged: usize,
1899    /// Retained iterative SCCs (#368) that had no dirty member at request begin and were therefore not
1900    /// re-run: their last exact fixed point is served as-is. Counted at
1901    /// request begin, so a demand-driven request that never reaches a
1902    /// retained SCC still reports it as reused.
1903    pub reused_sccs: usize,
1904    /// Members of the SCCs counted in `reused_sccs`.
1905    pub reused_scc_members: usize,
1906    /// Total wall-clock time spent inside Runtime SCC tasks.
1907    pub elapsed_ms: u128,
1908}
1909
1910#[derive(Debug, Clone, Copy)]
1911struct ScheduleBuildMeta {
1912    candidate_vertices: usize,
1913    vdeps_vertices: usize,
1914    vdeps_edges: usize,
1915    builder_elapsed_ms: u128,
1916    used_virtual_schedule: bool,
1917    schedule_cache_hit: bool,
1918    schedule_cache_eligible: bool,
1919}
1920
1921#[cfg(any(test, feature = "benchmark_internal"))]
1922#[doc(hidden)]
1923#[derive(Debug, Clone, Default)]
1924pub struct RecalcReuseProbe {
1925    pub schedule_requests: usize,
1926    pub schedule_cache_hits: usize,
1927    pub schedule_cache_misses: usize,
1928    pub schedule_cache_ineligible: usize,
1929    pub schedule_builds: usize,
1930    /// Program 3 plan reuse: misses served by restricting the base schedule.
1931    pub schedule_base_restrictions: usize,
1932    pub schedule_shared_handles: usize,
1933    pub schedule_retained_bytes: usize,
1934    pub legacy_target_requests: usize,
1935    pub target_schedule_builds: usize,
1936    pub demand_builds: usize,
1937    pub demand_vertices: usize,
1938    pub demand_clean_formulas: usize,
1939    pub demand_explicit_edges: usize,
1940    pub demand_virtual_builder_calls: usize,
1941}
1942
1943#[cfg(any(test, feature = "benchmark_internal"))]
1944fn schedule_probe_retained_bytes(schedule: &crate::engine::Schedule) -> usize {
1945    fn vector_bytes<T>(values: &Vec<T>) -> usize {
1946        values.capacity() * std::mem::size_of::<T>()
1947    }
1948
1949    [
1950        vector_bytes(&schedule.units),
1951        vector_bytes(&schedule.layers),
1952        vector_bytes(&schedule.cycles),
1953    ]
1954    .into_iter()
1955    .chain(
1956        schedule
1957            .layers
1958            .iter()
1959            .map(|layer| vector_bytes(&layer.vertices)),
1960    )
1961    .chain(schedule.cycles.iter().map(vector_bytes))
1962    .sum()
1963}
1964
1965#[derive(Debug, Clone)]
1966struct CachedScheduleEntry {
1967    topology_epoch: u64,
1968    /// Authority `(store revision, rev.dyn)` the schedule was planned from
1969    /// (design §8.4; always 0 without `unified_authority`).
1970    authority_revision: (u64, u64),
1971    /// The request's vertex list as runs of consecutive ids (formula ids
1972    /// come in column runs, so a whole-workbook request is a few runs).
1973    candidate_vertices: VertexIdRuns,
1974    schedule: Arc<crate::engine::scheduler::Schedule>,
1975}
1976
1977/// A vertex list stored as `(first id, run length)` runs of consecutive
1978/// ids, in list order.
1979#[derive(Debug, Clone, Default)]
1980struct VertexIdRuns(Vec<(u32, u32)>);
1981
1982impl VertexIdRuns {
1983    fn from_slice(ids: &[VertexId]) -> Self {
1984        let mut runs: Vec<(u32, u32)> = Vec::new();
1985        for v in ids {
1986            match runs.last_mut() {
1987                Some((first, len)) if first.checked_add(*len) == Some(v.0) => *len += 1,
1988                _ => runs.push((v.0, 1)),
1989            }
1990        }
1991        runs.shrink_to_fit();
1992        Self(runs)
1993    }
1994
1995    fn equals(&self, ids: &[VertexId]) -> bool {
1996        let mut rest = ids;
1997        for &(first, len) in &self.0 {
1998            let len = len as usize;
1999            if rest.len() < len {
2000                return false;
2001            }
2002            let (head, tail) = rest.split_at(len);
2003            if head
2004                .iter()
2005                .enumerate()
2006                .any(|(i, v)| v.0 != first.wrapping_add(i as u32))
2007            {
2008                return false;
2009            }
2010            rest = tail;
2011        }
2012        rest.is_empty()
2013    }
2014
2015    fn heap_bytes(&self) -> usize {
2016        self.0.capacity() * std::mem::size_of::<(u32, u32)>()
2017    }
2018
2019    /// The number of ids.
2020    fn len(&self) -> usize {
2021        self.0.iter().map(|&(_, len)| len as usize).sum()
2022    }
2023}
2024
2025#[cfg(test)]
2026mod vertex_id_runs_tests {
2027    use super::{VertexId, VertexIdRuns};
2028
2029    #[test]
2030    fn runs_compare_like_the_list() {
2031        let ids = |v: &[u32]| v.iter().map(|&i| VertexId(i)).collect::<Vec<_>>();
2032        let list = ids(&[5, 6, 7, 2, 3, 9, 10, 10]);
2033        let runs = VertexIdRuns::from_slice(&list);
2034        assert_eq!(runs.0, vec![(5, 3), (2, 2), (9, 2), (10, 1)]);
2035        assert!(runs.equals(&list));
2036        assert!(!runs.equals(&list[..7]));
2037        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 11])));
2038        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 10, 11])));
2039        assert!(VertexIdRuns::from_slice(&[]).equals(&[]));
2040        assert!(!VertexIdRuns::from_slice(&[]).equals(&list));
2041        let edge = ids(&[u32::MAX - 1, u32::MAX, 0]);
2042        assert!(VertexIdRuns::from_slice(&edge).equals(&edge));
2043    }
2044}
2045
2046/// Uncacheable requests keep their schedule inline without a shared allocation.
2047enum EvaluationSchedule {
2048    Owned(crate::engine::scheduler::Schedule),
2049    Shared(Arc<crate::engine::scheduler::Schedule>),
2050}
2051
2052impl std::ops::Deref for EvaluationSchedule {
2053    type Target = crate::engine::scheduler::Schedule;
2054
2055    fn deref(&self) -> &Self::Target {
2056        match self {
2057            Self::Owned(schedule) => schedule,
2058            Self::Shared(schedule) => schedule,
2059        }
2060    }
2061}
2062
2063type ScheduleBuildOutput = (
2064    crate::engine::scheduler::Schedule,
2065    FxHashMap<VertexId, Vec<VertexId>>,
2066    ScheduleBuildMeta,
2067);
2068
2069type EvaluationScheduleBuildOutput = (
2070    EvaluationSchedule,
2071    FxHashMap<VertexId, Vec<VertexId>>,
2072    ScheduleBuildMeta,
2073);
2074
2075/// Opaque, revision-bound recalculation recipe.
2076#[derive(Debug)]
2077pub struct RecalcPlan {
2078    key: RecalcPlanKey,
2079    kind: RecalcPlanKind,
2080}
2081
2082#[derive(Debug)]
2083struct RecalcPlanKey {
2084    engine_token: Arc<()>,
2085    revisions: PlanningRevisionSnapshot,
2086}
2087
2088#[derive(Clone, Debug, PartialEq, Eq)]
2089struct PlanningRevisionSnapshot {
2090    engine_topology_epoch: u64,
2091    graph_topology_revision: u64,
2092    staged: u64,
2093    symbols: u64,
2094    semantic: u64,
2095    provider: Option<u64>,
2096    deterministic_mode: crate::engine::DeterministicMode,
2097    budgets: crate::engine::EvaluationBudgets,
2098}
2099
2100#[derive(Debug)]
2101enum RecalcPlanKind {
2102    CompatibilityFull {
2103        schedule: crate::engine::Schedule,
2104        has_dynamic_refs: bool,
2105    },
2106    Target {
2107        targets: Vec<crate::engine::EvaluationTarget>,
2108        scope: crate::engine::PrepareScope,
2109        topology: RecalcTopology,
2110        dynamic_policy: DynamicPlanPolicy,
2111    },
2112}
2113
2114#[derive(Debug)]
2115enum RecalcTopology {
2116    RunLocalRecipe,
2117    Workbook,
2118}
2119
2120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2121enum DynamicPlanPolicy {
2122    BoundedTargetReplan,
2123}
2124
2125impl RecalcPlan {
2126    /// Returns the retained compatibility schedule depth. Target plans retain a
2127    /// run-local recipe rather than a schedule, so their layer count is zero.
2128    pub fn layer_count(&self) -> usize {
2129        match &self.kind {
2130            RecalcPlanKind::CompatibilityFull { schedule, .. } => schedule.layers.len(),
2131            RecalcPlanKind::Target { .. } => 0,
2132        }
2133    }
2134
2135    pub fn has_dynamic_refs(&self) -> bool {
2136        match &self.kind {
2137            RecalcPlanKind::CompatibilityFull {
2138                has_dynamic_refs, ..
2139            } => *has_dynamic_refs,
2140            RecalcPlanKind::Target { .. } => false,
2141        }
2142    }
2143
2144    #[cfg(test)]
2145    pub(crate) fn force_stale_reasons_for_test(
2146        &mut self,
2147        reasons: &[formualizer_common::PlanStaleReason],
2148    ) {
2149        use formualizer_common::PlanStaleReason;
2150        for reason in reasons {
2151            match reason {
2152                PlanStaleReason::Engine => {
2153                    self.key.engine_token = Arc::new(());
2154                }
2155                PlanStaleReason::Provider => {
2156                    self.key.revisions.provider = Some(
2157                        self.key
2158                            .revisions
2159                            .provider
2160                            .unwrap_or_default()
2161                            .wrapping_add(1),
2162                    );
2163                }
2164                PlanStaleReason::Semantic => {
2165                    self.key.revisions.semantic = self.key.revisions.semantic.wrapping_add(1);
2166                }
2167                PlanStaleReason::Budget => {
2168                    let current = self.key.revisions.budgets.work.max_work_units;
2169                    self.key.revisions.budgets.work.max_work_units =
2170                        Some(current.unwrap_or_default().wrapping_add(1));
2171                }
2172                PlanStaleReason::Staged => {
2173                    self.key.revisions.staged = self.key.revisions.staged.wrapping_add(1);
2174                }
2175                PlanStaleReason::Symbols => {
2176                    self.key.revisions.symbols = self.key.revisions.symbols.wrapping_add(1);
2177                }
2178                PlanStaleReason::Graph => {
2179                    self.key.revisions.graph_topology_revision =
2180                        self.key.revisions.graph_topology_revision.wrapping_add(1);
2181                }
2182                _ => {}
2183            }
2184        }
2185    }
2186}
2187
2188#[cfg(any(test, feature = "test-support"))]
2189pub(crate) mod criteria_mask_test_hooks {
2190    use std::cell::Cell;
2191
2192    thread_local! {
2193        static MASK_CALLS_ROWS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
2194        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
2195        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
2196    }
2197
2198    pub(crate) fn take_mask_work() -> (usize, usize) {
2199        MASK_CALLS_ROWS.with(|c| c.replace((0, 0)))
2200    }
2201
2202    pub(crate) fn note_mask(rows: usize) {
2203        MASK_CALLS_ROWS.with(|c| {
2204            let (calls, work) = c.get();
2205            c.set((calls + 1, work + rows));
2206        });
2207    }
2208
2209    pub fn reset_text_segment_counters() {
2210        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
2211        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
2212    }
2213
2214    pub fn text_segment_counters() -> (usize, usize) {
2215        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
2216        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
2217        (a, b)
2218    }
2219
2220    pub(crate) fn inc_total() {
2221        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
2222    }
2223    pub(crate) fn inc_all_null() {
2224        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
2225    }
2226}
2227
2228#[cfg(test)]
2229pub(crate) mod visibility_mask_test_hooks {
2230    use std::cell::Cell;
2231
2232    thread_local! {
2233        static HITS: Cell<usize> = const { Cell::new(0) };
2234        static MISSES: Cell<usize> = const { Cell::new(0) };
2235        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
2236    }
2237
2238    pub fn reset() {
2239        HITS.with(|c| c.set(0));
2240        MISSES.with(|c| c.set(0));
2241        EVICTIONS.with(|c| c.set(0));
2242    }
2243
2244    pub fn counters() -> (usize, usize, usize) {
2245        let hits = HITS.with(|c| c.get());
2246        let misses = MISSES.with(|c| c.get());
2247        let evictions = EVICTIONS.with(|c| c.get());
2248        (hits, misses, evictions)
2249    }
2250
2251    pub(crate) fn inc_hit() {
2252        HITS.with(|c| c.set(c.get() + 1));
2253    }
2254
2255    pub(crate) fn inc_miss() {
2256        MISSES.with(|c| c.set(c.get() + 1));
2257    }
2258
2259    pub(crate) fn inc_eviction() {
2260        EVICTIONS.with(|c| c.set(c.get() + 1));
2261    }
2262}
2263
2264fn is_numeric_text_equality(pred: &crate::args::CriteriaPredicate) -> bool {
2265    match pred {
2266        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(text)) => {
2267            text.trim().parse::<f64>().is_ok_and(f64::is_finite)
2268        }
2269        _ => false,
2270    }
2271}
2272
2273fn compute_criteria_mask(
2274    view: &RangeView<'_>,
2275    col_in_view: usize,
2276    pred: &crate::args::CriteriaPredicate,
2277) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2278    use crate::compute_prelude::{boolean, cmp, concat_arrays};
2279    use arrow::compute::kernels::comparison::{ilike, nilike};
2280    use arrow_array::{
2281        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
2282    };
2283
2284    // Helper: apply a numeric predicate to a single Float64Array chunk
2285    fn apply_numeric_pred(
2286        chunk: &Float64Array,
2287        pred: &crate::args::CriteriaPredicate,
2288    ) -> Option<BooleanArray> {
2289        match pred {
2290            crate::args::CriteriaPredicate::Gt(n) => {
2291                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
2292            }
2293            crate::args::CriteriaPredicate::Ge(n) => {
2294                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2295            }
2296            crate::args::CriteriaPredicate::Lt(n) => {
2297                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
2298            }
2299            crate::args::CriteriaPredicate::Le(n) => {
2300                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2301            }
2302            crate::args::CriteriaPredicate::Eq(v) => match v {
2303                formualizer_common::LiteralValue::Number(x) => {
2304                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
2305                }
2306                formualizer_common::LiteralValue::Int(i) => {
2307                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2308                }
2309                _ => None,
2310            },
2311            crate::args::CriteriaPredicate::Ne(v) => match v {
2312                formualizer_common::LiteralValue::Number(x) => {
2313                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
2314                }
2315                formualizer_common::LiteralValue::Int(i) => {
2316                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2317                }
2318                _ => None,
2319            },
2320            _ => None,
2321        }
2322    }
2323
2324    // Check if this is a numeric predicate that can be applied per-chunk
2325    let is_numeric_pred = matches!(
2326        pred,
2327        crate::args::CriteriaPredicate::Gt(_)
2328            | crate::args::CriteriaPredicate::Ge(_)
2329            | crate::args::CriteriaPredicate::Lt(_)
2330            | crate::args::CriteriaPredicate::Le(_)
2331            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
2332            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
2333            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
2334            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
2335    );
2336
2337    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
2338    // This avoids materializing the full numeric column (64-bit per element) and instead
2339    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
2340    if is_numeric_pred {
2341        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2342        for res in view.numbers_slices() {
2343            let (_rs, _rl, cols_seg) = res.ok()?;
2344            if col_in_view < cols_seg.len() {
2345                let chunk = cols_seg[col_in_view].as_ref();
2346                let mask = apply_numeric_pred(chunk, pred)?;
2347                bool_parts.push(mask);
2348            }
2349        }
2350
2351        if bool_parts.is_empty() {
2352            return None;
2353        } else if bool_parts.len() == 1 {
2354            return Some(std::sync::Arc::new(bool_parts.remove(0)));
2355        } else {
2356            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
2357            let anys: Vec<&dyn arrow_array::Array> = bool_parts
2358                .iter()
2359                .map(|a| a as &dyn arrow_array::Array)
2360                .collect();
2361            let conc: ArrayRef = concat_arrays(&anys).ok()?;
2362            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2363            return Some(std::sync::Arc::new(ba));
2364        }
2365    }
2366
2367    // Wildcards and numeric text equality can match non-text cells. The lowered
2368    // base lane is text-only, unlike the scalar matcher. Keep the vectorized
2369    // path for text-only data, but cache a scalar-equivalent mask for mixed data.
2370    if is_numeric_text_equality(pred)
2371        || matches!(pred, crate::args::CriteriaPredicate::TextLike { .. })
2372    {
2373        for tags in view.type_tags_slices() {
2374            let (_, _, cols) = tags.ok()?;
2375            let tags = cols.get(col_in_view)?;
2376            if tags.values().iter().any(|tag| {
2377                *tag == crate::arrow_store::TypeTag::Empty as u8
2378                    || *tag == crate::arrow_store::TypeTag::Number as u8
2379                    || *tag == crate::arrow_store::TypeTag::Boolean as u8
2380            }) {
2381                let mut mask = BooleanBuilder::new();
2382                for chunk in view.iter_row_chunks() {
2383                    let chunk = chunk.ok()?;
2384                    for row in chunk.row_start..chunk.row_start + chunk.row_len {
2385                        mask.append_value(crate::builtins::criteria_match(
2386                            pred,
2387                            &view.get_cell(row, col_in_view),
2388                        ));
2389                    }
2390                }
2391                return Some(std::sync::Arc::new(mask.finish()));
2392            }
2393        }
2394    }
2395
2396    // TEXT PATH: build masks per row-chunk using lowered text slices.
2397    // This avoids concatenating full-string columns just to compute a boolean mask.
2398    let (text_kind, text_pat, empty_special) = match pred {
2399        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
2400            (0u8, t.to_lowercase(), t.is_empty())
2401        }
2402        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
2403            (1u8, t.to_lowercase(), false)
2404        }
2405        crate::args::CriteriaPredicate::TextLike {
2406            pattern,
2407            case_insensitive,
2408        } => {
2409            let p = if *case_insensitive {
2410                pattern.to_lowercase()
2411            } else {
2412                pattern.clone()
2413            };
2414            (2u8, p.replace('*', "%").replace('?', "_"), false)
2415        }
2416        _ => return None,
2417    };
2418
2419    let text_pat_is_empty = text_pat.is_empty();
2420    let ne_matches_blank = text_kind == 1 && !text_pat_is_empty;
2421    let pat = StringArray::new_scalar(text_pat);
2422    let mut bool_parts: Vec<BooleanArray> = Vec::new();
2423
2424    let mut tag_slices = view.type_tags_slices();
2425    for res in view.iter_row_chunks() {
2426        let cs = res.ok()?;
2427        if cs.row_len == 0 {
2428            continue;
2429        }
2430        #[cfg(test)]
2431        criteria_mask_test_hooks::inc_total();
2432
2433        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
2434        if col_in_view >= slices.len() {
2435            return None;
2436        }
2437
2438        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
2439        if empty_special || (text_kind == 1 && text_pat_is_empty) {
2440            let (tag_start, tag_len, tags) = tag_slices.next()?.ok()?;
2441            if tag_start != cs.row_start || tag_len != cs.row_len {
2442                return None;
2443            }
2444            let tags = tags.get(col_in_view)?;
2445            // A null text lane is not a blank cell: base numeric/boolean/error
2446            // cells also have null text. Consult the overlay-aware type tags,
2447            // and inspect strings only to distinguish empty text from text.
2448            let strings = seg_opt.and_then(|a| a.as_any().downcast_ref::<StringArray>());
2449            let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2450            for i in 0..cs.row_len {
2451                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8
2452                    || (tags.value(i) == crate::arrow_store::TypeTag::Text as u8
2453                        && strings.is_some_and(|s| s.is_valid(i) && s.value(i).is_empty()));
2454                bb.append_value(if text_kind == 0 { blank } else { !blank });
2455            }
2456            #[cfg(test)]
2457            if seg_opt.is_none() {
2458                criteria_mask_test_hooks::inc_all_null();
2459            }
2460            bool_parts.push(bb.finish());
2461            continue;
2462        }
2463        let seg = match seg_opt {
2464            Some(s) => s,
2465            None => {
2466                #[cfg(test)]
2467                criteria_mask_test_hooks::inc_all_null();
2468                if (text_kind == 0 && empty_special) || ne_matches_blank {
2469                    // Eq("") treats nulls (Empty) as equal.
2470                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2471                    bb.append_n(cs.row_len, true);
2472                    bool_parts.push(bb.finish());
2473                } else {
2474                    // For non-empty patterns, ilike/nilike return null on null inputs.
2475                    bool_parts.push(BooleanArray::new_null(cs.row_len));
2476                }
2477                continue;
2478            }
2479        };
2480
2481        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
2482        let mut m = match text_kind {
2483            0 => ilike(seg_sa, &pat).ok()?,
2484            1 => nilike(seg_sa, &pat).ok()?,
2485            2 => ilike(seg_sa, &pat).ok()?,
2486            _ => return None,
2487        };
2488
2489        // Only fold blank/Empty (null) cells into the mask when the segment
2490        // actually contains any. The null-fill loop + or_kleene are pure
2491        // overhead on blank-free chunks, so a `<>text` (or `=""`) aggregation
2492        // over a column with no blanks stays fully vectorized on the ilike/
2493        // nilike result.
2494        if ((text_kind == 0 && empty_special) || ne_matches_blank) && seg_sa.null_count() > 0 {
2495            // Treat nulls as equal to empty string
2496            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
2497            for i in 0..seg_sa.len() {
2498                bb.append_value(seg_sa.is_null(i));
2499            }
2500            let nulls = bb.finish();
2501            m = boolean::or_kleene(&m, &nulls).ok()?;
2502        }
2503
2504        bool_parts.push(m);
2505    }
2506
2507    if bool_parts.is_empty() {
2508        None
2509    } else if bool_parts.len() == 1 {
2510        Some(std::sync::Arc::new(bool_parts.remove(0)))
2511    } else {
2512        let anys: Vec<&dyn arrow_array::Array> = bool_parts
2513            .iter()
2514            .map(|a| a as &dyn arrow_array::Array)
2515            .collect();
2516        let conc: ArrayRef = concat_arrays(&anys).ok()?;
2517        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2518        Some(std::sync::Arc::new(ba))
2519    }
2520}
2521
2522#[derive(Debug, Clone)]
2523pub struct LayerInfo {
2524    pub vertex_count: usize,
2525    pub parallel_eligible: bool,
2526    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
2527}
2528
2529#[derive(Debug, Clone)]
2530pub struct EvalPlan {
2531    pub total_vertices_to_evaluate: usize,
2532    pub layers: Vec<LayerInfo>,
2533    pub cycles_detected: usize,
2534    pub dirty_count: usize,
2535    pub volatile_count: usize,
2536    pub parallel_enabled: bool,
2537    pub estimated_parallel_layers: usize,
2538    pub target_cells: Vec<String>,
2539}
2540
2541/// Whether the configured FormulaPlane mode is ignored (spans never placed):
2542/// always, since the dependency authority is the runtime path (design §10).
2543#[inline]
2544fn plane_mode_ignored() -> bool {
2545    true
2546}
2547
2548/// Test-support probe for sibling-crate tests: true when span placement is
2549/// off because the FormulaPlane mode is ignored (see `plane_mode_ignored`).
2550#[cfg(feature = "test-support")]
2551#[doc(hidden)]
2552pub fn formula_plane_mode_ignored_for_test() -> bool {
2553    plane_mode_ignored()
2554}
2555
2556impl<R> Engine<R>
2557where
2558    R: EvaluationContext,
2559{
2560    /// # Panics
2561    /// Panics when `config.cycle` is invalid ([`CycleConfig::validate`],
2562    /// spec §2): `Iterate` with `detection: Static`, `max_iterations == 0`,
2563    /// or a negative/non-finite `max_change`. `EvalConfig::with_cycle`
2564    /// rejects these at build; this re-validates configs assembled via
2565    /// struct literals.
2566    pub fn new(resolver: R, config: EvalConfig) -> Self {
2567        // Under the unified authority the FormulaPlane mode is accepted and
2568        // ignored (design §10). Normalizing the stored mode keeps external
2569        // readers of `config` (loaders choosing a span-preparation route) on
2570        // the per-cell path; engine reads go through `formula_plane_mode()`.
2571        let config = if plane_mode_ignored() {
2572            EvalConfig {
2573                formula_plane_mode: FormulaPlaneMode::Off,
2574                ..config
2575            }
2576        } else {
2577            config
2578        };
2579        if let Err(msg) = config.cycle.validate() {
2580            panic!("invalid CycleConfig: {msg}");
2581        }
2582        crate::builtins::load_builtins();
2583        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2584            &config.evaluation_budgets,
2585            config.max_vertices,
2586            config.max_memory_mb,
2587            config.max_eval_time,
2588        );
2589
2590        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2591            #[cfg(feature = "system-clock")]
2592            {
2593                Arc::new(crate::timezone::SystemClock::new(
2594                    crate::timezone::TimeZoneSpec::default(),
2595                ))
2596            }
2597            #[cfg(not(feature = "system-clock"))]
2598            {
2599                Arc::new(crate::timezone::FixedClock::new(
2600                    chrono::DateTime::UNIX_EPOCH,
2601                    crate::timezone::TimeZoneSpec::Utc,
2602                ))
2603            }
2604        });
2605
2606        // Initialize thread pool based on config
2607        let thread_pool = if config.enable_parallel {
2608            let mut builder = ThreadPoolBuilder::new();
2609            if let Some(max_threads) = config.max_threads {
2610                builder = builder.num_threads(max_threads);
2611            }
2612
2613            match builder.build() {
2614                Ok(pool) => Some(Arc::new(pool)),
2615                Err(_) => {
2616                    // Fall back to sequential evaluation if thread pool creation fails
2617                    None
2618                }
2619            }
2620        } else {
2621            None
2622        };
2623
2624        // C1a retained/cache budgets are observational; cache defaults stay explicit.
2625        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
2626        let function_provider_revision_seen = resolver.planning_semantic_revision();
2627        let mut engine = Self {
2628            graph: DependencyGraph::new_with_config(config.clone()),
2629            resolver,
2630            config,
2631            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
2632            clock: crate::timezone::SnapshotClock::new(clock),
2633            thread_pool,
2634            recalc_epoch: 0,
2635            snapshot_id: std::sync::atomic::AtomicU64::new(1),
2636            topology_epoch: 0,
2637            cached_static_schedule: None,
2638            recent_schedules: Vec::new(),
2639            base_schedule: None,
2640            #[cfg(any(test, feature = "benchmark_internal"))]
2641            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
2642            spill_mgr: ShimSpillManager::default(),
2643            arrow_sheets: SheetStore::default(),
2644            format_registry: crate::format::FormatRegistry::default(),
2645            derived_formats: Default::default(),
2646            #[cfg(test)]
2647            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
2648            #[cfg(test)]
2649            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
2650            #[cfg(test)]
2651            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
2652            #[cfg(test)]
2653            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
2654            #[cfg(test)]
2655            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
2656            #[cfg(test)]
2657            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
2658            #[cfg(test)]
2659            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
2660            #[cfg(test)]
2661            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
2662            compressed_at_build: None,
2663            #[cfg(test)]
2664            computed_overlay_set_explicit_entry_operations_for_test: 0,
2665            #[cfg(test)]
2666            computed_overlay_stale_clear_range_effects_for_test: 0,
2667            #[cfg(test)]
2668            computed_overlay_stale_clear_offset_attempts_for_test: 0,
2669            #[cfg(test)]
2670            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
2671            has_edited: false,
2672            overlay_compactions: 0,
2673            computed_overlay_bytes_estimate: 0,
2674            computed_overlay_mirroring_disabled: false,
2675            force_materialize_range_views: false,
2676            row_bounds_cache: std::sync::RwLock::new(None),
2677            used_axis_bounds_cache: std::sync::RwLock::new(None),
2678            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
2679            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
2680            source_formula_token: Arc::new(()),
2681            recalc_plan_token: Arc::new(()),
2682            staged_formulas: std::collections::HashMap::new(),
2683            staged_formula_index: StagedFormulaIndex::default(),
2684            blocked_pending_spills: Vec::new(),
2685            row_visibility: FxHashMap::default(),
2686            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
2687            formula_parse_diagnostics: Vec::new(),
2688            last_formula_ingest_report: None,
2689            formula_ingest_report_total: FormulaIngestReport::default(),
2690            active_cancel_flag: None,
2691            active_evaluation_deadline: None,
2692            action_depth: 0,
2693            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
2694            virtual_dep_fallback_activations: 0,
2695            last_cycle_telemetry: CycleTelemetry::default(),
2696            next_evaluation_resource_request_id: 1,
2697            evaluation_resource_request_depth: 0,
2698            active_evaluation_resource_request: None,
2699            last_evaluation_resource_request: None,
2700            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
2701            evaluation_resource_request_started_at: None,
2702            evaluation_resource_budgets: resolved_resources.budgets,
2703            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
2704            active_resource_ledger: None,
2705            source_cache_footprints: Vec::new(),
2706            source_cache_accounted: 0,
2707            pending_iterative_redirty: Vec::new(),
2708            retained_scc_members: FxHashMap::default(),
2709            next_retained_scc_id: 0,
2710            retained_scc_config_fingerprint: 0,
2711            retained_scc_function_epoch_seen: 0,
2712            retained_scc_provider_revision_seen: None,
2713            retained_scc_dirty_at_begin: Vec::new(),
2714            iterative_state_values: FxHashMap::default(),
2715            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
2716            function_provider_revision_seen,
2717            #[cfg(feature = "tracing")]
2718            trace_evaluation_counters: TraceEvaluationCounters::default(),
2719            #[cfg(test)]
2720            evaluation_request_begin_count_for_test: 0,
2721            #[cfg(any(test, feature = "test-support"))]
2722            before_prepared_span_commit_hook: None,
2723            #[cfg(test)]
2724            before_target_preparation_commit_hook: None,
2725            #[cfg(test)]
2726            before_target_planning_snapshot_hook: None,
2727            #[cfg(test)]
2728            inject_target_semantic_stale_once_for_test: false,
2729            #[cfg(test)]
2730            force_virtual_dep_changes_remaining_for_test: 0,
2731            freshness: Default::default(),
2732            #[cfg(test)]
2733            fail_evaluation_commit_preflight_once_for_test: false,
2734            #[cfg(test)]
2735            target_preparation_fault_for_test: None,
2736            #[cfg(test)]
2737            force_non_cycle_schedule_fallback_for_test: false,
2738            #[cfg(test)]
2739            before_legacy_fallback_final_provider_sample_hook: None,
2740            #[cfg(test)]
2741            after_eager_proposal_commit_hook: None,
2742        };
2743        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
2744        engine.config.arrow_storage_enabled = true;
2745        engine.config.delta_overlay_enabled = true;
2746        engine.config.write_formula_overlay_enabled = true;
2747        let default_sheet = engine.graph.default_sheet_name().to_string();
2748        engine.ensure_arrow_sheet(&default_sheet);
2749        engine
2750    }
2751
2752    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
2753    ///
2754    /// # Panics
2755    /// Panics when `config.cycle` is invalid, exactly like [`Engine::new`].
2756    pub fn with_thread_pool(
2757        resolver: R,
2758        config: EvalConfig,
2759        thread_pool: Arc<rayon::ThreadPool>,
2760    ) -> Self {
2761        if let Err(msg) = config.cycle.validate() {
2762            panic!("invalid CycleConfig: {msg}");
2763        }
2764        crate::builtins::load_builtins();
2765        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2766            &config.evaluation_budgets,
2767            config.max_vertices,
2768            config.max_memory_mb,
2769            config.max_eval_time,
2770        );
2771        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2772            #[cfg(feature = "system-clock")]
2773            {
2774                Arc::new(crate::timezone::SystemClock::new(
2775                    crate::timezone::TimeZoneSpec::default(),
2776                ))
2777            }
2778            #[cfg(not(feature = "system-clock"))]
2779            {
2780                Arc::new(crate::timezone::FixedClock::new(
2781                    chrono::DateTime::UNIX_EPOCH,
2782                    crate::timezone::TimeZoneSpec::Utc,
2783                ))
2784            }
2785        });
2786        // C1a retained/cache budgets are observational; cache defaults stay explicit.
2787        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
2788        let function_provider_revision_seen = resolver.planning_semantic_revision();
2789        let mut engine = Self {
2790            graph: DependencyGraph::new_with_config(config.clone()),
2791            resolver,
2792            config,
2793            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
2794            clock: crate::timezone::SnapshotClock::new(clock),
2795            thread_pool: Some(thread_pool),
2796            recalc_epoch: 0,
2797            snapshot_id: std::sync::atomic::AtomicU64::new(1),
2798            topology_epoch: 0,
2799            cached_static_schedule: None,
2800            recent_schedules: Vec::new(),
2801            base_schedule: None,
2802            #[cfg(any(test, feature = "benchmark_internal"))]
2803            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
2804            spill_mgr: ShimSpillManager::default(),
2805            arrow_sheets: SheetStore::default(),
2806            format_registry: crate::format::FormatRegistry::default(),
2807            derived_formats: Default::default(),
2808            #[cfg(test)]
2809            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
2810            #[cfg(test)]
2811            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
2812            #[cfg(test)]
2813            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
2814            #[cfg(test)]
2815            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
2816            #[cfg(test)]
2817            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
2818            #[cfg(test)]
2819            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
2820            #[cfg(test)]
2821            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
2822            #[cfg(test)]
2823            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
2824            compressed_at_build: None,
2825            #[cfg(test)]
2826            computed_overlay_set_explicit_entry_operations_for_test: 0,
2827            #[cfg(test)]
2828            computed_overlay_stale_clear_range_effects_for_test: 0,
2829            #[cfg(test)]
2830            computed_overlay_stale_clear_offset_attempts_for_test: 0,
2831            #[cfg(test)]
2832            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
2833            has_edited: false,
2834            overlay_compactions: 0,
2835            computed_overlay_bytes_estimate: 0,
2836            computed_overlay_mirroring_disabled: false,
2837            force_materialize_range_views: false,
2838            row_bounds_cache: std::sync::RwLock::new(None),
2839            used_axis_bounds_cache: std::sync::RwLock::new(None),
2840            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
2841            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
2842            source_formula_token: Arc::new(()),
2843            recalc_plan_token: Arc::new(()),
2844            staged_formulas: std::collections::HashMap::new(),
2845            staged_formula_index: StagedFormulaIndex::default(),
2846            blocked_pending_spills: Vec::new(),
2847            row_visibility: FxHashMap::default(),
2848            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
2849            formula_parse_diagnostics: Vec::new(),
2850            last_formula_ingest_report: None,
2851            formula_ingest_report_total: FormulaIngestReport::default(),
2852            active_cancel_flag: None,
2853            active_evaluation_deadline: None,
2854            action_depth: 0,
2855            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
2856            virtual_dep_fallback_activations: 0,
2857            last_cycle_telemetry: CycleTelemetry::default(),
2858            next_evaluation_resource_request_id: 1,
2859            evaluation_resource_request_depth: 0,
2860            active_evaluation_resource_request: None,
2861            last_evaluation_resource_request: None,
2862            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
2863            evaluation_resource_request_started_at: None,
2864            evaluation_resource_budgets: resolved_resources.budgets,
2865            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
2866            active_resource_ledger: None,
2867            source_cache_footprints: Vec::new(),
2868            source_cache_accounted: 0,
2869            pending_iterative_redirty: Vec::new(),
2870            retained_scc_members: FxHashMap::default(),
2871            next_retained_scc_id: 0,
2872            retained_scc_config_fingerprint: 0,
2873            retained_scc_function_epoch_seen: 0,
2874            retained_scc_provider_revision_seen: None,
2875            retained_scc_dirty_at_begin: Vec::new(),
2876            iterative_state_values: FxHashMap::default(),
2877            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
2878            function_provider_revision_seen,
2879            #[cfg(feature = "tracing")]
2880            trace_evaluation_counters: TraceEvaluationCounters::default(),
2881            #[cfg(test)]
2882            evaluation_request_begin_count_for_test: 0,
2883            #[cfg(any(test, feature = "test-support"))]
2884            before_prepared_span_commit_hook: None,
2885            #[cfg(test)]
2886            before_target_preparation_commit_hook: None,
2887            #[cfg(test)]
2888            before_target_planning_snapshot_hook: None,
2889            #[cfg(test)]
2890            inject_target_semantic_stale_once_for_test: false,
2891            #[cfg(test)]
2892            force_virtual_dep_changes_remaining_for_test: 0,
2893            freshness: Default::default(),
2894            #[cfg(test)]
2895            fail_evaluation_commit_preflight_once_for_test: false,
2896            #[cfg(test)]
2897            target_preparation_fault_for_test: None,
2898            #[cfg(test)]
2899            force_non_cycle_schedule_fallback_for_test: false,
2900            #[cfg(test)]
2901            before_legacy_fallback_final_provider_sample_hook: None,
2902            #[cfg(test)]
2903            after_eager_proposal_commit_hook: None,
2904        };
2905        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
2906        engine.config.arrow_storage_enabled = true;
2907        engine.config.delta_overlay_enabled = true;
2908        engine.config.write_formula_overlay_enabled = true;
2909        let default_sheet = engine.graph.default_sheet_name().to_string();
2910        engine.ensure_arrow_sheet(&default_sheet);
2911        engine
2912    }
2913
2914    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
2915        &self.workbook_load_limits
2916    }
2917
2918    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
2919        self.workbook_load_limits = limits;
2920    }
2921
2922    fn clear_source_cache(&self) {
2923        if let Ok(mut g) = self.source_cache.write() {
2924            *g = SourceCache::default();
2925        }
2926    }
2927
2928    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
2929        &self.last_virtual_dep_telemetry
2930    }
2931
2932    /// Telemetry from Runtime SCC evaluation during the most recent
2933    /// evaluation request (always default-zero under `CycleDetection::Static`
2934    /// or when `enable_virtual_dep_telemetry` is off).
2935    pub fn last_cycle_telemetry(&self) -> &CycleTelemetry {
2936        &self.last_cycle_telemetry
2937    }
2938
2939    /// Resource observations for the most recently completed public evaluation request.
2940    pub fn last_evaluation_resource_request_stats(
2941        &self,
2942    ) -> Option<&EvaluationResourceRequestStats> {
2943        self.last_evaluation_resource_request.as_ref()
2944    }
2945
2946    /// Cumulative resource observations since engine creation or the last telemetry reset.
2947    pub fn evaluation_resource_baseline_stats(&self) -> EvaluationResourceBaselineStats {
2948        self.evaluation_resource_baseline
2949    }
2950
2951    pub fn evaluation_resource_budgets(&self) -> &crate::engine::EvaluationBudgets {
2952        &self.evaluation_resource_budgets
2953    }
2954
2955    /// At most one diagnostic is emitted for deprecated resource fields.
2956    pub fn evaluation_resource_config_diagnostic(
2957        &self,
2958    ) -> Option<&crate::engine::EvaluationResourceConfigDiagnostic> {
2959        self.evaluation_resource_config_diagnostic.as_ref()
2960    }
2961
2962    /// Reset accumulated and last-request observations without reusing request IDs.
2963    pub fn reset_evaluation_resource_telemetry(&mut self) {
2964        self.evaluation_resource_baseline = EvaluationResourceBaselineStats::default();
2965        self.last_evaluation_resource_request = None;
2966    }
2967
2968    // Reconcile without replay locks: several packages may share the same Arc.
2969    // Weak tokens neither retain dead packages nor duplicate their allocations.
2970    fn reconcile_source_cache_footprints(&mut self) -> Result<(), ExcelError> {
2971        self.blocked_pending_spills.retain(|&(vertex, anchor, _)| {
2972            self.graph.vertex_exists(vertex)
2973                && self.graph.get_cell_ref(vertex) == Some(anchor)
2974                && matches!(
2975                    self.graph.get_vertex_kind(vertex),
2976                    VertexKind::FormulaScalar | VertexKind::FormulaArray
2977                )
2978        });
2979        if self.blocked_pending_spills.is_empty() {
2980            self.blocked_pending_spills = Vec::new();
2981        }
2982        let mut bytes = (self.blocked_pending_spills.capacity()
2983            * std::mem::size_of::<(VertexId, CellRef, Region)>()) as u64;
2984        self.source_cache_footprints.retain(|weak| {
2985            let Some(footprint) = weak.upgrade() else {
2986                return false;
2987            };
2988            bytes = bytes.saturating_add(footprint.load(std::sync::atomic::Ordering::Acquire));
2989            true
2990        });
2991        if let Some(ledger) = self.active_resource_ledger.as_mut() {
2992            ledger
2993                .release_retained(self.source_cache_accounted)
2994                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
2995            // Observe first: even a tightened budget cannot erase live ownership.
2996            ledger.observe_retained(bytes);
2997            self.source_cache_accounted = bytes;
2998            ledger
2999                .reserve_retained(0)
3000                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3001        }
3002        Ok(())
3003    }
3004
3005    fn duration_ns(duration: std::time::Duration) -> u64 {
3006        u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3007    }
3008
3009    fn observe_evaluation_resource_request<T>(
3010        &mut self,
3011        kind: EvaluationRequestKind,
3012        evaluate: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3013    ) -> Result<T, ExcelError> {
3014        let outermost = self.evaluation_resource_request_depth == 0;
3015        #[cfg(feature = "tracing")]
3016        if outermost {
3017            self.trace_evaluation_counters = TraceEvaluationCounters::default();
3018        }
3019        #[cfg(feature = "tracing")]
3020        let request_kind = if matches!(
3021            kind,
3022            EvaluationRequestKind::Full
3023                | EvaluationRequestKind::FullWithDelta
3024                | EvaluationRequestKind::FullCancellable
3025                | EvaluationRequestKind::FullLogged
3026        ) {
3027            "full"
3028        } else {
3029            "targeted"
3030        };
3031        #[cfg(feature = "tracing")]
3032        let _request_span = outermost.then(|| {
3033            crate::engine::trace::fz_span!(
3034                tracing::Level::INFO,
3035                "evaluate",
3036                "evaluate.request",
3037                kind = request_kind,
3038                mode = ?FormulaPlaneMode::Off
3039            )
3040        });
3041        if outermost {
3042            let request_id = self.next_evaluation_resource_request_id;
3043            self.next_evaluation_resource_request_id = request_id
3044                .checked_add(1)
3045                .expect("evaluation resource request ID exhausted");
3046            self.active_evaluation_resource_request = Some(EvaluationResourceRequestStats::new(
3047                request_id,
3048                kind,
3049                FormulaPlaneMode::Off,
3050                self.staged_formula_count(),
3051            ));
3052            self.evaluation_resource_baseline.record_started(request_id);
3053            self.evaluation_resource_request_started_at = Some(crate::instant::FzInstant::now());
3054            self.source_cache_accounted = 0;
3055            self.active_resource_ledger = Some(ResourceLedger::new(
3056                Some(request_id),
3057                self.evaluation_resource_budgets.clone(),
3058            ));
3059        }
3060        self.evaluation_resource_request_depth =
3061            self.evaluation_resource_request_depth.saturating_add(1);
3062        let result = if outermost {
3063            self.reconcile_source_cache_footprints()
3064                .and_then(|()| self.resource_checkpoint(0))
3065                .and_then(|()| evaluate(self))
3066        } else {
3067            evaluate(self)
3068        };
3069        if outermost && result.is_err() {
3070            self.freshness_abort_pass();
3071        }
3072        self.evaluation_resource_request_depth =
3073            self.evaluation_resource_request_depth.saturating_sub(1);
3074
3075        let reconciliation = if outermost {
3076            self.reconcile_source_cache_footprints()
3077        } else {
3078            Ok(())
3079        };
3080        let result = result.and_then(|value| reconciliation.map(|()| value));
3081        if outermost {
3082            let total_ns = self
3083                .evaluation_resource_request_started_at
3084                .take()
3085                .map(|start| Self::duration_ns(start.elapsed()))
3086                .unwrap_or(0);
3087            let mut stats = self
3088                .active_evaluation_resource_request
3089                .take()
3090                .expect("outer evaluation resource request has active stats");
3091            let mut ledger = self
3092                .active_resource_ledger
3093                .take()
3094                .expect("outer evaluation resource request has active ledger");
3095            ledger.release_all_scratch();
3096            stats.ledger.update(ledger.snapshot());
3097            stats.outcome = match &result {
3098                Ok(_) => EvaluationRequestOutcome::Success,
3099                Err(error) if error.kind == ExcelErrorKind::Cancelled => {
3100                    EvaluationRequestOutcome::Cancelled
3101                }
3102                Err(_) => EvaluationRequestOutcome::Error,
3103            };
3104            if stats.dirty_lease == FormulaDirtyLeaseOutcome::Acquired {
3105                stats.dirty_lease = if stats.outcome == EvaluationRequestOutcome::Cancelled {
3106                    FormulaDirtyLeaseOutcome::RetainedOnCancellation
3107                } else {
3108                    FormulaDirtyLeaseOutcome::RetainedOnError
3109                };
3110            }
3111            stats.phases.total_ns = total_ns;
3112            let attributed = stats
3113                .phases
3114                .staged_prepare_ns
3115                .saturating_add(stats.phases.topology_ns)
3116                .saturating_add(stats.phases.materialization_ns);
3117            stats.phases.evaluation_ns = total_ns.saturating_sub(attributed);
3118            self.evaluation_resource_baseline.record_finished(&stats);
3119            self.last_evaluation_resource_request = Some(stats);
3120            crate::engine::trace::fz_event!(
3121                tracing::Level::INFO,
3122                "evaluate",
3123                "evaluate.summary",
3124                computed_vertices = self.trace_evaluation_counters.computed_vertices,
3125                cycles = self.trace_evaluation_counters.cycles,
3126                cancelled = matches!(
3127                    &result,
3128                    Err(error) if error.kind == ExcelErrorKind::Cancelled
3129                )
3130            );
3131        }
3132        result
3133    }
3134
3135    pub fn set_evaluation_resource_budgets(&mut self, budgets: crate::engine::EvaluationBudgets) {
3136        self.evaluation_resource_budgets = budgets.clone();
3137        self.config.evaluation_budgets = budgets.clone();
3138        self.graph.set_evaluation_budgets(budgets);
3139    }
3140
3141    #[cfg(test)]
3142    pub(crate) fn set_evaluation_budgets_for_test(
3143        &mut self,
3144        budgets: crate::engine::EvaluationBudgets,
3145    ) {
3146        self.set_evaluation_resource_budgets(budgets);
3147    }
3148
3149    fn preflight_evaluation_commit_window(
3150        &mut self,
3151        bounded_writes: usize,
3152    ) -> Result<crate::instant::FzInstant, ExcelError> {
3153        #[cfg(test)]
3154        if std::mem::take(&mut self.fail_evaluation_commit_preflight_once_for_test) {
3155            return Err(crate::engine::ResourceLedgerError::Exhausted(
3156                formualizer_common::ResourceExhaustionDetail {
3157                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3158                    limit: 0,
3159                    observed: 1,
3160                    request_id: self
3161                        .active_evaluation_resource_request
3162                        .as_ref()
3163                        .map(|stats| stats.request_id),
3164                },
3165            )
3166            .into_excel_error());
3167        }
3168        let estimate = std::time::Duration::from_nanos(
3169            u64::try_from(bounded_writes)
3170                .unwrap_or(u64::MAX)
3171                .saturating_mul(100),
3172        );
3173        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3174            ledger
3175                .preflight_commit_window(estimate)
3176                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3177        }
3178        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3179            stats.evaluation_commit_preflight_count =
3180                stats.evaluation_commit_preflight_count.saturating_add(1);
3181            stats.evaluation_commit_estimated_ns = stats
3182                .evaluation_commit_estimated_ns
3183                .saturating_add(Self::duration_ns(estimate));
3184        }
3185        Ok(crate::instant::FzInstant::now())
3186    }
3187
3188    fn observe_evaluation_commit_window(&mut self, started: crate::instant::FzInstant) {
3189        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3190            stats.evaluation_commit_actual_ns = stats
3191                .evaluation_commit_actual_ns
3192                .saturating_add(Self::duration_ns(started.elapsed()));
3193        }
3194    }
3195
3196    fn cancellation_checkpoint(&self, message: &'static str) -> Result<(), ExcelError> {
3197        if self
3198            .active_cancel_flag
3199            .as_ref()
3200            .is_some_and(|cancel| cancel.is_cancelled())
3201        {
3202            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3203        }
3204        if self
3205            .active_evaluation_deadline
3206            .is_some_and(|deadline| Instant::now() >= deadline)
3207        {
3208            return Err(crate::engine::ResourceLedgerError::Exhausted(
3209                formualizer_common::ResourceExhaustionDetail {
3210                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3211                    limit: 0,
3212                    observed: 1,
3213                    request_id: self
3214                        .active_evaluation_resource_request
3215                        .as_ref()
3216                        .map(|request| request.request_id),
3217                },
3218            )
3219            .into_excel_error()
3220            .with_message(message));
3221        }
3222        Ok(())
3223    }
3224
3225    fn resource_checkpoint(&mut self, work_units: u64) -> Result<(), ExcelError> {
3226        let Some(ledger) = self.active_resource_ledger.as_mut() else {
3227            return Ok(());
3228        };
3229        ledger
3230            .charge_work(work_units)
3231            .and_then(|()| ledger.checkpoint_deadline())
3232            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3233    }
3234
3235    fn charge_bounded_work(&mut self, mut work_units: u64) -> Result<(), ExcelError> {
3236        if work_units == 0 {
3237            return self.resource_checkpoint(0);
3238        }
3239        while work_units > 0 {
3240            let chunk = work_units.min(256);
3241            self.resource_checkpoint(chunk)?;
3242            work_units -= chunk;
3243        }
3244        Ok(())
3245    }
3246
3247    fn reserve_request_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3248        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3249            // Exact request topology and activation of the scratch cap are C1b. C1a records
3250            // scoped ownership but must not introduce a new skip or terminal path.
3251            ledger.observe_scratch(bytes);
3252        }
3253        Ok(())
3254    }
3255
3256    fn release_request_scratch(&mut self, bytes: u64) {
3257        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3258            let released = ledger.release_scratch(bytes);
3259            debug_assert!(
3260                released.is_ok(),
3261                "request scratch release exceeded the outstanding reservation"
3262            );
3263        }
3264    }
3265
3266    fn reserve_topology_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3267        self.active_resource_ledger
3268            .as_mut()
3269            .map_or(Ok(()), |ledger| ledger.reserve_schedule_discovery(bytes))
3270            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3271    }
3272
3273    fn reserve_graph_source_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3274        self.active_resource_ledger
3275            .as_mut()
3276            .map_or(Ok(()), |ledger| ledger.reserve_graph_source(bytes))
3277            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3278    }
3279
3280    fn with_request_scratch<T>(
3281        &mut self,
3282        bytes: u64,
3283        work: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3284    ) -> Result<T, ExcelError> {
3285        self.reserve_request_scratch(bytes)?;
3286        let result = work(self);
3287        self.release_request_scratch(bytes);
3288        result
3289    }
3290
3291    fn observe_staged_preparation(
3292        &mut self,
3293        selected: usize,
3294        retained: usize,
3295        elapsed: std::time::Duration,
3296    ) {
3297        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3298            stats.staged_selected = stats.staged_selected.saturating_add(selected as u64);
3299            stats.staged_retained = retained as u64;
3300            stats.phases.staged_prepare_ns = stats
3301                .phases
3302                .staged_prepare_ns
3303                .saturating_add(Self::duration_ns(elapsed));
3304        }
3305    }
3306
3307    fn graph_admission_enabled(&self) -> bool {
3308        crate::engine::resource_ledger::graph_admission_enabled(&self.evaluation_resource_budgets)
3309    }
3310
3311    fn preflight_graph_admission(
3312        &mut self,
3313        usage: crate::engine::resource_ledger::GraphAdmission,
3314    ) -> Result<(), ExcelError> {
3315        let request_id = self
3316            .active_evaluation_resource_request
3317            .as_ref()
3318            .map(|stats| stats.request_id);
3319        crate::engine::resource_ledger::preflight_graph_admission(
3320            &self.evaluation_resource_budgets,
3321            usage,
3322            request_id,
3323        )
3324        .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3325    }
3326
3327    fn prepared_legacy_admission(
3328        &mut self,
3329        plan: &PreparedLegacyGraphPlan,
3330        materialization_cells: u64,
3331    ) -> Result<(), ExcelError> {
3332        if !self.graph_admission_enabled() {
3333            return Ok(());
3334        }
3335        let stats = self.graph.baseline_stats();
3336        let added_edges = plan.planned_edge_count().ok_or_else(|| {
3337            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3338        })?;
3339        let removed_edges = plan.removed_edge_count().ok_or_else(|| {
3340            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3341        })?;
3342        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
3343            final_vertices: stats
3344                .graph_vertex_count
3345                .checked_add(plan.new_vertex_count())
3346                .ok_or_else(|| {
3347                    ExcelError::new(ExcelErrorKind::NImpl)
3348                        .with_message("graph vertex count overflow")
3349                })?,
3350            final_edges: stats
3351                .graph_edge_count
3352                .checked_sub(removed_edges)
3353                .and_then(|count| count.checked_add(added_edges))
3354                .ok_or_else(|| {
3355                    ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3356                })?,
3357            materialization_cells,
3358            added_vertices: plan.new_vertex_count(),
3359            added_edges,
3360        })
3361    }
3362
3363    fn observe_target_admission_failure(
3364        &mut self,
3365        reason: formualizer_common::ResourceExhaustionReason,
3366    ) {
3367        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3368            stats.target_admission_failure = Some(reason);
3369        }
3370    }
3371
3372    fn observe_target_preparation_report(
3373        &mut self,
3374        report: &crate::engine::PreparedTargetGraphReport,
3375    ) {
3376        let reason_bit = |reason: crate::engine::OpaqueReason| -> u64 {
3377            let index = match reason {
3378                crate::engine::OpaqueReason::DynamicReference => 0,
3379                crate::engine::OpaqueReason::RuntimeTextReference => 1,
3380                crate::engine::OpaqueReason::UnknownFunction => 2,
3381                crate::engine::OpaqueReason::UnknownCustomFunction => 3,
3382                crate::engine::OpaqueReason::UnresolvedCrossSheetBinding => 4,
3383                crate::engine::OpaqueReason::UnresolvedName => 5,
3384                crate::engine::OpaqueReason::UnresolvedTable => 6,
3385                crate::engine::OpaqueReason::FormulaName => 7,
3386                crate::engine::OpaqueReason::DeferredSourcePackage => 8,
3387                crate::engine::OpaqueReason::UnsupportedSourceSemantics => 9,
3388                crate::engine::OpaqueReason::UncertainDefaultSheetBinding => 10,
3389            };
3390            1u64 << index
3391        };
3392        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3393            stats.staged_selected = report.selected_staged_cells as u64;
3394            stats.staged_retained = report.retained_staged_cells as u64;
3395            stats.target_requested = report.requested_targets as u64;
3396            stats.target_normalized_regions = report.normalized_regions as u64;
3397            stats.target_scope_level = match &report.widened_scope {
3398                crate::engine::PrepareScope::Exact => 0,
3399                crate::engine::PrepareScope::Sheets(_) => 1,
3400                crate::engine::PrepareScope::Workbook => 2,
3401            };
3402            stats.target_widening_reason_bits = report
3403                .widening_reasons
3404                .iter()
3405                .copied()
3406                .fold(0, |bits, reason| bits | reason_bit(reason));
3407            stats.graph_source_scratch_estimated = report.estimated_scratch_bytes;
3408            stats.graph_source_scratch_observed = report.observed_scratch_bytes;
3409            stats.target_commit_estimated_work = report.estimated_commit_work;
3410            stats.target_commit_actual_work = report.actual_commit_work;
3411            stats.target_commit_window_ns = Self::duration_ns(report.commit_window);
3412            stats.phases.staged_prepare_ns = stats
3413                .phases
3414                .staged_prepare_ns
3415                .saturating_add(Self::duration_ns(report.commit_window));
3416        }
3417    }
3418
3419    /// Begin a new evaluation request: reset per-recalc cycle telemetry and
3420    /// take the per-recalc volatile clock sample. Called at the start of
3421    /// every evaluation request that walks schedule units.
3422    fn begin_evaluation_request(&mut self) {
3423        self.freshness_begin_request();
3424        #[cfg(test)]
3425        {
3426            self.evaluation_request_begin_count_for_test = self
3427                .evaluation_request_begin_count_for_test
3428                .saturating_add(1);
3429        }
3430        self.last_cycle_telemetry = CycleTelemetry::default();
3431        self.graph.authority_sync();
3432        // Defensive: consumed at the end of the previous request; a request
3433        // that errored out mid-walk must not leak its members into this one.
3434        self.pending_iterative_redirty.clear();
3435        self.reconcile_retained_sccs_at_request_begin();
3436        // Spec §7.11: NOW()/TODAY() sample the clock ONCE per recalc; every
3437        // read within this request (including SCC iteration passes) observes
3438        // this sample.
3439        self.clock.refresh();
3440    }
3441
3442    /// End-of-recalc redirty: volatile vertices (as always) plus members of
3443    /// SCCs that iterated this recalc without reaching a retainable fixed
3444    /// point (`CyclePolicy::Iterate`), so circular cells re-evaluate on every
3445    /// recalc exactly like Excel's iterative calculation (spec §4
3446    /// persistence / §7.6 accumulator / §7.11 volatile redirty). Retained
3447    /// SCCs (`retained_scc_members`, #368) are left clean. Replaces the bare
3448    /// `graph.redirty_volatiles()` call at every evaluation-flow exit; must
3449    /// run AFTER the flow's `clear_dirty_flags`.
3450    fn redirty_for_next_recalc(&mut self) {
3451        self.graph.redirty_volatiles();
3452        let pending = std::mem::take(&mut self.pending_iterative_redirty);
3453        let dirty_at_begin = std::mem::take(&mut self.retained_scc_dirty_at_begin);
3454        for (vertex, scc) in dirty_at_begin {
3455            if self.retained_scc_members.get(&vertex) == Some(&scc) {
3456                // No SCC task claimed this member during the request: the
3457                // cycle dissolved and it evaluated as an ordinary formula,
3458                // or the request never reached it. Either way it is no
3459                // longer a retained fixed point. Its persisted value is
3460                // obsolete only if it actually re-evaluated (clean now).
3461                self.retained_scc_members.remove(&vertex);
3462                if !self.graph.is_dirty(vertex) {
3463                    self.iterative_state_values.remove(&vertex);
3464                }
3465            }
3466        }
3467        if !self.iterative_state_values.is_empty() || !self.retained_scc_members.is_empty() {
3468            let graph = &self.graph;
3469            self.iterative_state_values
3470                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3471            self.retained_scc_members
3472                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3473        }
3474        // Refresh the §4-persistence snapshot for members that re-run each
3475        // recalc: these final values survive structural edits that clear the
3476        // computed overlay (the only value home in canonical mode) so the
3477        // next SCC task can re-seed from them (see `iterative_state_values`).
3478        for &vertex in &pending {
3479            if !self.graph.is_live_formula_vertex(vertex) {
3480                continue;
3481            }
3482            if let Some(cell) = self.graph.get_cell_ref(vertex) {
3483                let sheet_name = self.graph.sheet_name(cell.sheet_id);
3484                match self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1) {
3485                    Some(value) if !matches!(value, LiteralValue::Empty) => {
3486                        self.iterative_state_values.insert(vertex, value);
3487                    }
3488                    _ => {
3489                        self.iterative_state_values.remove(&vertex);
3490                    }
3491                }
3492            }
3493        }
3494        if !pending.is_empty() {
3495            self.graph.redirty_iterative_members(&pending);
3496        }
3497    }
3498
3499    /// Hash of every `EvalConfig` knob that can change the result of a
3500    /// retained SCC without any edit reaching the dependency graph. The
3501    /// function registry is tracked separately and precisely (see
3502    /// `retained_scc_function_epoch_seen`).
3503    fn retained_scc_config_fingerprint(&self) -> u64 {
3504        use std::hash::{Hash, Hasher};
3505        let mut hasher = rustc_hash::FxHasher::default();
3506        let config = &self.config;
3507        std::mem::discriminant(&config.cycle.detection).hash(&mut hasher);
3508        match config.cycle.policy {
3509            CyclePolicy::Error => 0u8.hash(&mut hasher),
3510            CyclePolicy::Iterate {
3511                max_iterations,
3512                max_change,
3513            } => {
3514                1u8.hash(&mut hasher);
3515                max_iterations.hash(&mut hasher);
3516                max_change.to_bits().hash(&mut hasher);
3517            }
3518        }
3519        config.date_system.hash(&mut hasher);
3520        config.workbook_seed.hash(&mut hasher);
3521        std::mem::discriminant(&config.volatile_level).hash(&mut hasher);
3522        // `DeterministicMode` carries a timestamp and a timezone spec; hash
3523        // its Debug rendering rather than growing its derive set for this.
3524        format!("{:?}", config.deterministic_mode).hash(&mut hasher);
3525        config.range_expansion_limit.hash(&mut hasher);
3526        config.max_open_ended_rows.hash(&mut hasher);
3527        config.max_open_ended_cols.hash(&mut hasher);
3528        hasher.finish()
3529    }
3530
3531    /// Request-begin bookkeeping for retained SCCs (#368): drop deleted
3532    /// vertices, invalidate everything when the config fingerprint moved
3533    /// (marking retained members dirty so their SCC tasks run in this
3534    /// request), and record how many retained SCCs are being reused, i.e.
3535    /// have no dirty member at request begin.
3536    fn reconcile_retained_sccs_at_request_begin(&mut self) {
3537        self.retained_scc_dirty_at_begin.clear();
3538        if self.retained_scc_members.is_empty() {
3539            return;
3540        }
3541        // Deleted vertices and members overwritten with a literal (the
3542        // vertex survives as a value cell) are no longer retained formulas.
3543        let graph = &self.graph;
3544        self.retained_scc_members
3545            .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3546        if self.retained_scc_config_fingerprint() != self.retained_scc_config_fingerprint {
3547            let members: Vec<VertexId> = self.retained_scc_members.keys().copied().collect();
3548            self.retained_scc_members.clear();
3549            self.graph.mark_dirty_many(&members);
3550            return;
3551        }
3552        let changes =
3553            crate::function_registry::semantic_changes_since(self.retained_scc_function_epoch_seen);
3554        let global_changed = changes.epoch != self.retained_scc_function_epoch_seen;
3555        let provider_revision = self.resolver.planning_semantic_revision();
3556        let provider_changed = provider_revision != self.retained_scc_provider_revision_seen;
3557        if global_changed || provider_changed {
3558            let changed: BTreeSet<(String, String)> = changes.keys.into_iter().collect();
3559            let affected: Vec<VertexId> = self
3560                .retained_scc_members
3561                .keys()
3562                .copied()
3563                .filter(|&vertex| {
3564                    let Some(ast) = self.graph.get_formula(vertex) else {
3565                        return true;
3566                    };
3567                    (global_changed
3568                        && (!changes.complete || Self::ast_uses_changed_function(&ast, &changed)))
3569                        || (provider_changed && Self::ast_contains_function(&ast))
3570                })
3571                .collect();
3572            for vertex in &affected {
3573                self.retained_scc_members.remove(vertex);
3574            }
3575            if !affected.is_empty() {
3576                self.graph.mark_dirty_many(&affected);
3577            }
3578            self.retained_scc_function_epoch_seen = changes.epoch;
3579            self.retained_scc_provider_revision_seen = provider_revision;
3580        }
3581        let mut dirty_sccs: FxHashSet<u64> = FxHashSet::default();
3582        let mut all_sccs: FxHashSet<u64> = FxHashSet::default();
3583        for (&vertex, &scc) in &self.retained_scc_members {
3584            all_sccs.insert(scc);
3585            if self.graph.is_dirty(vertex) {
3586                dirty_sccs.insert(scc);
3587                self.retained_scc_dirty_at_begin.push((vertex, scc));
3588            }
3589        }
3590        let reused_members = self
3591            .retained_scc_members
3592            .values()
3593            .filter(|scc| !dirty_sccs.contains(scc))
3594            .count();
3595        let t = &mut self.last_cycle_telemetry;
3596        t.reused_sccs = all_sccs.len() - dirty_sccs.len();
3597        t.reused_scc_members = reused_members;
3598    }
3599
3600    pub fn virtual_dep_fallback_activations(&self) -> u64 {
3601        self.virtual_dep_fallback_activations
3602    }
3603
3604    #[cfg(test)]
3605    pub(crate) fn lookup_index_flights_built_for_test(&self) -> usize {
3606        self.lookup_index_cache
3607            .flights_built
3608            .load(std::sync::atomic::Ordering::Relaxed)
3609    }
3610
3611    pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
3612        self.lookup_index_cache.report()
3613    }
3614
3615    fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
3616        let start_row = view.start_row();
3617        let end_row = view.end_row();
3618        let start_col = view.start_col();
3619        let end_col = view.end_col();
3620        for row in start_row..=end_row {
3621            let Ok(row_u32) = u32::try_from(row) else {
3622                return true;
3623            };
3624            for col in start_col..=end_col {
3625                let Ok(col_u32) = u32::try_from(col) else {
3626                    return true;
3627                };
3628                let cell_ref = self
3629                    .graph
3630                    .make_cell_ref_internal(sheet_id, row_u32, col_u32);
3631                if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
3632                    && self.graph.is_volatile(vertex_id)
3633                {
3634                    return true;
3635                }
3636            }
3637        }
3638        false
3639    }
3640
3641    fn build_lookup_index_impl(
3642        &self,
3643        view: &RangeView<'_>,
3644        axis: LookupAxis,
3645    ) -> Option<Arc<LookupIndex>> {
3646        let (rows, cols) = view.dims();
3647        if rows == 0 || cols == 0 {
3648            self.lookup_index_cache.note_skipped_tiny();
3649            return None;
3650        }
3651        let len = match axis {
3652            LookupAxis::ColumnInView(col) => {
3653                if col >= cols {
3654                    self.lookup_index_cache.note_skipped_tiny();
3655                    return None;
3656                }
3657                rows
3658            }
3659            LookupAxis::RowInView(row) => {
3660                if row >= rows {
3661                    self.lookup_index_cache.note_skipped_tiny();
3662                    return None;
3663                }
3664                cols
3665            }
3666        };
3667        if len < 64 {
3668            self.lookup_index_cache.note_skipped_tiny();
3669            return None;
3670        }
3671
3672        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
3673        let key = LookupIndexKey {
3674            sheet_id,
3675            start_row: u32::try_from(view.start_row()).ok()?,
3676            start_col: u32::try_from(view.start_col()).ok()?,
3677            end_row: u32::try_from(view.end_row()).ok()?,
3678            end_col: u32::try_from(view.end_col()).ok()?,
3679            axis,
3680            snapshot_id: self.data_snapshot_id(),
3681        };
3682        if let Some(index) = self.lookup_index_cache.get(&key) {
3683            return Some(index);
3684        }
3685        if self
3686            .lookup_index_cache
3687            .would_exceed_cap(estimate_bytes(len, 0))
3688        {
3689            self.lookup_index_cache.note_skipped_cap();
3690            return None;
3691        }
3692        if !self.lookup_index_cache.should_build(key) {
3693            return None;
3694        }
3695        // Parallel members of a lookup family miss together: one builds.
3696        self.lookup_index_cache.single_flight(key, || {
3697            if let Some(index) = self.lookup_index_cache.recheck(&key) {
3698                return Some(index);
3699            }
3700            if self.lookup_index_cache.is_known_volatile(&key) {
3701                self.lookup_index_cache.note_skipped_volatile();
3702                return None;
3703            }
3704            if self.lookup_view_contains_volatile(view, sheet_id) {
3705                self.lookup_index_cache.note_volatile_key(key);
3706                self.lookup_index_cache.note_skipped_volatile();
3707                return None;
3708            }
3709            #[cfg(test)]
3710            self.lookup_index_cache
3711                .flights_built
3712                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3713            match LookupIndex::build(view, axis, self.config.date_system).ok()? {
3714                BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
3715                BuildOutcome::ErrorInLookupAxis => {
3716                    self.lookup_index_cache.note_skipped_error();
3717                    None
3718                }
3719                BuildOutcome::Degenerate => {
3720                    self.lookup_index_cache.note_skipped_tiny();
3721                    None
3722                }
3723            }
3724        })
3725    }
3726
3727    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
3728        if !self.config.enable_virtual_dep_telemetry {
3729            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
3730                fallback_mode_activations: self.virtual_dep_fallback_activations,
3731                ..VirtualDepTelemetry::default()
3732            };
3733        }
3734    }
3735
3736    fn source_cache_session(&self) -> SourceCacheSession {
3737        self.clear_source_cache();
3738        SourceCacheSession {
3739            cache: self.source_cache.clone(),
3740        }
3741    }
3742
3743    fn resolve_source_scalar_cached(
3744        &self,
3745        name: &str,
3746        version: Option<u64>,
3747    ) -> Result<LiteralValue, ExcelError> {
3748        let key = (name.to_string(), version);
3749        if let Ok(mut g) = self.source_cache.write() {
3750            if let Some(v) = g.scalars.get(&key) {
3751                return Ok(v.clone());
3752            }
3753
3754            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
3755                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3756                    ExcelError::new(ExcelErrorKind::Ref)
3757                        .with_message(format!("Unresolved source scalar: {name}"))
3758                } else {
3759                    err
3760                }
3761            })?;
3762            g.scalars.insert(key, v.clone());
3763            Ok(v)
3764        } else {
3765            self.resolver.resolve_source_scalar(name).map_err(|err| {
3766                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3767                    ExcelError::new(ExcelErrorKind::Ref)
3768                        .with_message(format!("Unresolved source scalar: {name}"))
3769                } else {
3770                    err
3771                }
3772            })
3773        }
3774    }
3775
3776    fn resolve_source_table_cached(
3777        &self,
3778        name: &str,
3779        version: Option<u64>,
3780    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
3781        let key = (name.to_string(), version);
3782        if let Ok(mut g) = self.source_cache.write() {
3783            if let Some(t) = g.tables.get(&key) {
3784                return Ok(t.clone());
3785            }
3786
3787            let t = self.resolver.resolve_source_table(name).map_err(|err| {
3788                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3789                    ExcelError::new(ExcelErrorKind::Ref)
3790                        .with_message(format!("Unresolved source table: {name}"))
3791                } else {
3792                    err
3793                }
3794            })?;
3795            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
3796            g.tables.insert(key, t.clone());
3797            Ok(t)
3798        } else {
3799            self.resolver
3800                .resolve_source_table(name)
3801                .map_err(|err| {
3802                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3803                        ExcelError::new(ExcelErrorKind::Ref)
3804                            .with_message(format!("Unresolved source table: {name}"))
3805                    } else {
3806                        err
3807                    }
3808                })
3809                .map(Arc::from)
3810        }
3811    }
3812
3813    fn source_table_to_range_view(
3814        &self,
3815        table: &dyn crate::traits::Table,
3816        spec: &Option<formualizer_parse::parser::TableSpecifier>,
3817    ) -> Result<RangeView<'static>, ExcelError> {
3818        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
3819
3820        let owned = match spec {
3821            Some(TableSpecifier::Column(c)) => {
3822                let c = c.trim();
3823                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
3824                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
3825                        "Complex structured references not yet supported".to_string(),
3826                    ));
3827                }
3828                table.get_column(c)?.materialise().into_owned()
3829            }
3830            Some(TableSpecifier::ColumnRange(start, end)) => {
3831                let cols = table.columns();
3832                let start = start.trim();
3833                let end = end.trim();
3834                let start_key = start.to_lowercase();
3835                let end_key = end.to_lowercase();
3836                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
3837                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
3838                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
3839                    if si > ei {
3840                        std::mem::swap(&mut si, &mut ei);
3841                    }
3842                    let h = table.data_height();
3843                    let w = ei - si + 1;
3844                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
3845                    for (offset, ci) in (si..=ei).enumerate() {
3846                        let cname = &cols[ci];
3847                        let col_range = table.get_column(cname)?;
3848                        let (rh, _) = col_range.dimensions();
3849                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
3850                            row[offset] = col_range.get(r, 0)?;
3851                        }
3852                    }
3853                    rows
3854                } else {
3855                    return Err(ExcelError::new(ExcelErrorKind::Ref)
3856                        .with_message("Column range refers to unknown column(s)".to_string()));
3857                }
3858            }
3859            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
3860            | Some(TableSpecifier::Headers) => table
3861                .headers_row()
3862                .map(|r| r.materialise().into_owned())
3863                .unwrap_or_default(),
3864            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
3865            | Some(TableSpecifier::Totals) => table
3866                .totals_row()
3867                .map(|r| r.materialise().into_owned())
3868                .unwrap_or_default(),
3869            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
3870                table
3871                    .data_body()
3872                    .map(|r| r.materialise().into_owned())
3873                    .unwrap_or_default()
3874            }
3875            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
3876                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
3877                if let Some(h) = table.headers_row() {
3878                    out.extend(h.iter_rows());
3879                }
3880                if let Some(body) = table.data_body() {
3881                    out.extend(body.iter_rows());
3882                }
3883                if let Some(tr) = table.totals_row() {
3884                    out.extend(tr.iter_rows());
3885                }
3886                out
3887            }
3888            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
3889                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
3890                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
3891                ));
3892            }
3893            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
3894                return Err(ExcelError::new(ExcelErrorKind::NImpl)
3895                    .with_message("Complex structured references not yet supported".to_string()));
3896            }
3897            None => {
3898                return Err(ExcelError::new(ExcelErrorKind::NImpl)
3899                    .with_message("Table reference without specifier is unsupported".to_string()));
3900            }
3901        };
3902
3903        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
3904    }
3905
3906    pub fn default_sheet_id(&self) -> SheetId {
3907        self.graph.default_sheet_id()
3908    }
3909
3910    pub fn default_sheet_name(&self) -> &str {
3911        self.graph.default_sheet_name()
3912    }
3913
3914    /// Update the workbook seed for deterministic RNGs in functions.
3915    pub fn set_workbook_seed(&mut self, seed: u64) {
3916        self.config.workbook_seed = seed;
3917    }
3918
3919    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
3920    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
3921        self.config.volatile_level = level;
3922    }
3923
3924    /// Set public temporal materialisation to native values or raw serials.
3925    pub fn set_temporal_egress(&mut self, policy: crate::engine::TemporalEgress) {
3926        self.config.temporal_egress = policy;
3927    }
3928
3929    pub fn temporal_egress(&self) -> crate::engine::TemporalEgress {
3930        self.config.temporal_egress
3931    }
3932
3933    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
3934    pub fn set_deterministic_mode(
3935        &mut self,
3936        mode: crate::engine::DeterministicMode,
3937    ) -> Result<(), ExcelError> {
3938        let clock = mode.build_clock()?;
3939        self.config.deterministic_mode = mode;
3940        self.clock = crate::timezone::SnapshotClock::new(clock);
3941        Ok(())
3942    }
3943
3944    /// Inject a custom [`ClockProvider`](crate::timezone::ClockProvider) for
3945    /// volatile date/time builtins (`NOW()`, `TODAY()`).
3946    ///
3947    /// The provider is the clock *source*; per spec §7.11 the engine samples
3948    /// it once at the start of every evaluation request and all reads within
3949    /// that recalc (including SCC iteration passes) observe the frozen
3950    /// sample.
3951    pub fn set_clock(&mut self, clock: Arc<dyn crate::timezone::ClockProvider>) {
3952        self.clock = crate::timezone::SnapshotClock::new(clock);
3953    }
3954
3955    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
3956        self.config.deterministic_mode.validate()
3957    }
3958
3959    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
3960        self.graph.sheet_id(name)
3961    }
3962
3963    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
3964        self.add_sheet(name)
3965            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
3966    }
3967
3968    pub fn sheet_name(&self, id: SheetId) -> &str {
3969        self.graph.sheet_name(id)
3970    }
3971
3972    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
3973        let id = self.graph.add_sheet(name)?;
3974        self.ensure_arrow_sheet(name);
3975        self.mark_topology_edited();
3976        Ok(id)
3977    }
3978
3979    pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
3980        let source_id = self.graph.sheet_id(source).ok_or_else(|| {
3981            ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
3982        })?;
3983        if new_name.is_empty() || new_name.len() > 255 {
3984            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
3985        }
3986        if self.graph.sheet_id(new_name).is_some() {
3987            return Err(ExcelError::new(ExcelErrorKind::Value)
3988                .with_message(format!("Sheet '{new_name}' already exists")));
3989        }
3990        let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
3991
3992        if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
3993            let mut copied_sheet = source_sheet;
3994            copied_sheet.name = Arc::<str>::from(new_name);
3995            self.arrow_sheets.sheets.push(copied_sheet);
3996        } else {
3997            self.ensure_arrow_sheet(new_name);
3998        }
3999
4000        let duplicated_formulas = self
4001            .graph
4002            .formula_vertices()
4003            .into_iter()
4004            .filter(|vertex| {
4005                self.graph
4006                    .get_cell_ref(*vertex)
4007                    .is_some_and(|cell| cell.sheet_id == new_id)
4008            })
4009            .collect::<Vec<_>>();
4010        self.graph.mark_vertices_dirty_batch(&duplicated_formulas);
4011        self.mark_topology_edited();
4012        Ok(new_id)
4013    }
4014
4015    fn ensure_arrow_sheet(&mut self, name: &str) {
4016        if self.arrow_sheets.sheet(name).is_some() {
4017            return;
4018        }
4019        self.arrow_sheets
4020            .sheets
4021            .push(crate::arrow_store::ArrowSheet {
4022                name: std::sync::Arc::<str>::from(name),
4023                date_system: self.config.date_system,
4024                columns: Vec::new(),
4025                nrows: 0,
4026                chunk_starts: Vec::new(),
4027                chunk_rows: 32 * 1024,
4028            });
4029    }
4030
4031    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
4032        let name = self.graph.sheet_name(sheet_id).to_string();
4033        self.purge_derived_formats_for_sheet(sheet_id);
4034        self.graph.remove_sheet(sheet_id)?;
4035        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
4036        // Sheet removal can change cross-sheet refs, names, and default-sheet
4037        // resolution. Until those domains have a complete exact dependency
4038        // proof, retain the documented graph-owned global invalidation.
4039        self.clear_all_computed_overlays();
4040        self.mark_all_formula_vertices_dirty();
4041        self.clear_staged_formulas_for_sheet(&name);
4042        if self.row_visibility.remove(&sheet_id).is_some() {
4043            self.invalidate_row_visibility_mask_cache();
4044        }
4045        self.record_structural_change(StructuralScope::RemovedSheet(sheet_id));
4046        self.mark_topology_edited();
4047        Ok(())
4048    }
4049
4050    /// Helper to synchronize the Arrow-backed storage layer.
4051    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
4052        if let Some(asheet) = self
4053            .arrow_sheets
4054            .sheets
4055            .iter_mut()
4056            .find(|s| s.name.as_ref() == target_name)
4057        {
4058            asheet.name = std::sync::Arc::<str>::from(new_name);
4059            return true;
4060        }
4061        false
4062    }
4063
4064    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
4065        let old_name = self.graph.sheet_name(sheet_id).to_string();
4066
4067        // Speculative Storage Update
4068        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
4069        self.rename_sheet_in_arrow_store(&old_name, new_name);
4070
4071        // Graph Update (Metadata + Rescue Logic)
4072        match self.graph.rename_sheet(sheet_id, new_name) {
4073            Ok(_) => {
4074                self.rename_staged_formula_sheet(&old_name, new_name);
4075                // Success! Invalidate cache for the moved sheet
4076                let sheet_vertices: Vec<VertexId> = self
4077                    .graph
4078                    .grid_vertices_in_sheet(sheet_id)
4079                    .map(|(id, _)| id)
4080                    .collect();
4081                for v_id in sheet_vertices {
4082                    self.graph.mark_vertex_dirty(v_id);
4083                }
4084                // Sheet rename preserves SheetId and therefore formula dependencies.
4085                self.mark_topology_edited();
4086                Ok(())
4087            }
4088            Err(e) => {
4089                // ROLLBACK: Revert storage if graph rejected the name
4090                self.rename_sheet_in_arrow_store(new_name, &old_name);
4091                Err(e)
4092            }
4093        }
4094    }
4095
4096    pub fn named_ranges_iter(
4097        &self,
4098    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
4099        self.graph.named_ranges_iter()
4100    }
4101
4102    pub fn sheet_named_ranges_iter(
4103        &self,
4104    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
4105        self.graph.sheet_named_ranges_iter()
4106    }
4107
4108    pub fn resolve_name_entry(
4109        &self,
4110        name: &str,
4111        current_sheet: SheetId,
4112    ) -> Option<&crate::engine::named_range::NamedRange> {
4113        self.graph.resolve_name_entry(name, current_sheet)
4114    }
4115
4116    /// The [`NameScope`] an optional scope-sheet argument denotes.
4117    ///
4118    /// `None` means **workbook scope**, not "the default sheet": a caller that
4119    /// supplies no sheet context is asking about workbook-scoped names only.
4120    /// An unknown sheet name is a malformed query and errors rather than
4121    /// silently degrading to another sheet's scope (issue #110).
4122    ///
4123    /// This is the one owned derivation from `Option<&str>` to a name scope;
4124    /// every scope-taking entry point routes through it.
4125    pub(crate) fn name_query_scope(
4126        &self,
4127        scope_sheet: Option<&str>,
4128    ) -> Result<NameScope, ExcelError> {
4129        match scope_sheet {
4130            None => Ok(NameScope::Workbook),
4131            Some(sheet) => self
4132                .graph
4133                .sheet_id(sheet)
4134                .map(NameScope::Sheet)
4135                .ok_or_else(|| {
4136                    ExcelError::new(ExcelErrorKind::Ref)
4137                        .with_message(format!("name scope sheet not found: {sheet}"))
4138                }),
4139        }
4140    }
4141
4142    /// Whether `name` resolves in the scope denoted by `scope_sheet`.
4143    ///
4144    /// `scope_sheet == None` asks about workbook scope only; a name scoped to a
4145    /// single sheet (including the default sheet) does not answer it. An unknown
4146    /// sheet name resolves nothing.
4147    /// Resolve a [`SharedSheetLocator`](crate::reference::SharedSheetLocator)
4148    /// against an explicit context sheet.
4149    ///
4150    /// Thin forwarder to
4151    /// [`SheetRegistry::resolve_locator`](crate::engine::sheet_registry::SheetRegistry::resolve_locator),
4152    /// the single owned derivation. `Current` resolves to `context_sheet`, never
4153    /// to the workbook's default sheet.
4154    fn resolve_sheet_locator(
4155        &self,
4156        locator: &crate::reference::SharedSheetLocator<'_>,
4157        context_sheet: SheetId,
4158    ) -> Result<SheetId, ExcelError> {
4159        self.graph
4160            .sheet_reg()
4161            .resolve_locator(locator, context_sheet)
4162    }
4163
4164    pub fn has_name(&self, name: &str, scope_sheet: Option<&str>) -> bool {
4165        let Ok(scope) = self.name_query_scope(scope_sheet) else {
4166            return false;
4167        };
4168        self.graph
4169            .resolve_name_entry_in_scope(name, scope)
4170            .is_some()
4171    }
4172
4173    /// The current value of `name` in the scope denoted by `scope_sheet`.
4174    ///
4175    /// Scoping follows [`Self::has_name`]: `None` is workbook scope only.
4176    pub fn resolved_name_value(
4177        &self,
4178        name: &str,
4179        scope_sheet: Option<&str>,
4180    ) -> Option<LiteralValue> {
4181        let scope = self.name_query_scope(scope_sheet).ok()?;
4182        let entry = self.graph.resolve_name_entry_in_scope(name, scope)?;
4183        self.graph.get_value(entry.vertex)
4184    }
4185
4186    pub fn table_metadata(&self, name: &str) -> Option<TableMetadata> {
4187        let entry = self.graph.resolve_table_entry(name)?;
4188        Some(TableMetadata {
4189            name: entry.name.clone(),
4190            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
4191            start_row: entry.range.start.coord.row() + 1,
4192            start_col: entry.range.start.coord.col() + 1,
4193            end_row: entry.range.end.coord.row() + 1,
4194            end_col: entry.range.end.coord.col() + 1,
4195            header_row: entry.header_row,
4196            headers: entry.headers.clone(),
4197            totals_row: entry.totals_row,
4198        })
4199    }
4200
4201    /// Metadata for every defined table, ordered by name.
4202    pub fn tables(&self) -> Vec<TableMetadata> {
4203        self.graph
4204            .table_names()
4205            .into_iter()
4206            .filter_map(|name| self.table_metadata(&name))
4207            .collect()
4208    }
4209
4210    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4211        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
4212
4213        for (name, named) in self.graph.named_ranges_iter() {
4214            out.push(crate::engine::named_range::NamedRangeSnapshot {
4215                name: name.clone(),
4216                scope: NameScope::Workbook,
4217                definition: named.definition.clone(),
4218            });
4219        }
4220
4221        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
4222            out.push(crate::engine::named_range::NamedRangeSnapshot {
4223                name: name.clone(),
4224                scope: NameScope::Sheet(*sheet_id),
4225                definition: named.definition.clone(),
4226            });
4227        }
4228
4229        out.sort_by(|a, b| {
4230            let a_scope = match a.scope {
4231                NameScope::Workbook => (0u8, 0u32),
4232                NameScope::Sheet(id) => (1u8, u32::from(id)),
4233            };
4234            let b_scope = match b.scope {
4235                NameScope::Workbook => (0u8, 0u32),
4236                NameScope::Sheet(id) => (1u8, u32::from(id)),
4237            };
4238            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
4239        });
4240
4241        out
4242    }
4243
4244    pub fn named_ranges_snapshot_for_sheet(
4245        &self,
4246        sheet_id: SheetId,
4247    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4248        self.named_ranges_snapshot()
4249            .into_iter()
4250            .filter(|entry| match entry.scope {
4251                NameScope::Workbook => true,
4252                NameScope::Sheet(id) => id == sheet_id,
4253            })
4254            .collect()
4255    }
4256
4257    pub fn define_name(
4258        &mut self,
4259        name: &str,
4260        definition: NamedDefinition,
4261        scope: NameScope,
4262    ) -> Result<(), ExcelError> {
4263        self.graph.validate_define_name(name, scope)?;
4264        self.graph.define_name(name, definition, scope)?;
4265        self.record_structural_change(StructuralScope::AllSheets);
4266
4267        self.mark_topology_edited();
4268
4269        Ok(())
4270    }
4271
4272    pub fn update_name(
4273        &mut self,
4274        name: &str,
4275        definition: NamedDefinition,
4276        scope: NameScope,
4277    ) -> Result<(), ExcelError> {
4278        self.graph.validate_existing_name(name, scope)?;
4279        self.graph.update_name(name, definition, scope)?;
4280        self.record_structural_change(StructuralScope::AllSheets);
4281
4282        self.mark_topology_edited();
4283
4284        Ok(())
4285    }
4286
4287    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
4288        self.graph.validate_existing_name(name, scope)?;
4289        self.graph.delete_name(name, scope)?;
4290        self.record_structural_change(StructuralScope::AllSheets);
4291
4292        self.mark_topology_edited();
4293
4294        Ok(())
4295    }
4296
4297    pub fn define_table(
4298        &mut self,
4299        name: &str,
4300        range: crate::reference::RangeRef,
4301        header_row: bool,
4302        headers: Vec<String>,
4303        totals_row: bool,
4304    ) -> Result<(), ExcelError> {
4305        self.graph
4306            .define_table(name, range, header_row, headers, totals_row)?;
4307        self.record_structural_change(StructuralScope::AllSheets);
4308        self.mark_topology_edited();
4309        Ok(())
4310    }
4311
4312    pub fn define_source_scalar(
4313        &mut self,
4314        name: &str,
4315        version: Option<u64>,
4316    ) -> Result<(), ExcelError> {
4317        self.graph.define_source_scalar(name, version)?;
4318        self.record_structural_change(StructuralScope::OpaqueGlobal);
4319        self.mark_topology_edited();
4320        Ok(())
4321    }
4322
4323    pub fn define_source_table(
4324        &mut self,
4325        name: &str,
4326        version: Option<u64>,
4327    ) -> Result<(), ExcelError> {
4328        self.graph.define_source_table(name, version)?;
4329        self.record_structural_change(StructuralScope::OpaqueGlobal);
4330        self.mark_topology_edited();
4331        Ok(())
4332    }
4333
4334    pub fn set_source_scalar_version(
4335        &mut self,
4336        name: &str,
4337        version: Option<u64>,
4338    ) -> Result<(), ExcelError> {
4339        self.graph.set_source_scalar_version(name, version)?;
4340        Ok(())
4341    }
4342
4343    pub fn set_source_table_version(
4344        &mut self,
4345        name: &str,
4346        version: Option<u64>,
4347    ) -> Result<(), ExcelError> {
4348        self.graph.set_source_table_version(name, version)?;
4349        Ok(())
4350    }
4351
4352    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
4353        self.graph.invalidate_source(name)?;
4354        Ok(())
4355    }
4356
4357    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
4358        self.graph.get_value(vertex)
4359    }
4360
4361    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
4362        self.graph.get_cell_value(sheet, row, col)
4363    }
4364
4365    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
4366        self.graph.get_vertex_for_cell(cell)
4367    }
4368
4369    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
4370        self.graph.get_evaluation_vertices()
4371    }
4372
4373    /// Return read-only baseline counters for dispatch benchmarking.
4374    pub fn baseline_stats(&self) -> EngineBaselineStats {
4375        let graph = self.graph.baseline_stats();
4376        EngineBaselineStats {
4377            graph_vertex_count: graph.graph_vertex_count,
4378            graph_formula_vertex_count: graph.graph_formula_vertex_count,
4379            graph_edge_count: graph.graph_edge_count,
4380            dirty_vertex_count: graph.dirty_vertex_count,
4381            evaluation_vertex_count: graph.evaluation_vertex_count,
4382            formula_ast_root_count: graph.formula_ast_root_count,
4383            formula_ast_node_count: graph.formula_ast_node_count,
4384            staged_formula_count: self.staged_formula_count(),
4385            formula_plane_active_span_count: 0,
4386            formula_plane_producer_result_entries: 0,
4387            formula_plane_consumer_read_entries: 0,
4388            formula_plane_mixed_topology_cache_builds: 0,
4389            formula_plane_mixed_topology_cache_hits: 0,
4390            formula_plane_mixed_topology_cache_overflows: 0,
4391            formula_plane_dirty_pending_events: 0,
4392            formula_plane_dirty_region_events_recorded: 0,
4393            formula_plane_dirty_span_region_events_recorded: 0,
4394            formula_plane_dirty_whole_span_seeds_recorded: 0,
4395            formula_plane_dirty_global_invalidations: 0,
4396            formula_plane_structural_span_candidates: 0,
4397            formula_plane_cycle_member_span_demotions: 0,
4398            formula_plane_array_result_span_demotions: 0,
4399            retained_scc_members: self.retained_scc_members.len(),
4400        }
4401    }
4402
4403    /// Mutation revision captured by read-only engine reports.
4404    pub(crate) fn inspection_mutation_revision(&self) -> u64 {
4405        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
4406    }
4407
4408    #[cfg(test)]
4409    pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
4410        self.used_axis_bounds_cache
4411            .read()
4412            .ok()
4413            .and_then(|guard| {
4414                guard.as_ref().map(|cache| {
4415                    (
4416                        cache.row_hits.load(Ordering::Relaxed),
4417                        cache.row_misses.load(Ordering::Relaxed),
4418                        cache.col_hits.load(Ordering::Relaxed),
4419                        cache.col_misses.load(Ordering::Relaxed),
4420                    )
4421                })
4422            })
4423            .unwrap_or((0, 0, 0, 0))
4424    }
4425
4426    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
4427        self.graph.set_first_load_assume_new(enabled);
4428    }
4429
4430    pub fn first_load_assume_new(&self) -> bool {
4431        self.graph.first_load_assume_new()
4432    }
4433
4434    pub fn reset_ensure_touched(&mut self) {
4435        self.graph.reset_ensure_touched();
4436    }
4437
4438    pub fn finalize_sheet_index(&mut self, sheet: &str) {
4439        self.graph.finalize_sheet_index(sheet);
4440    }
4441
4442    /// Execute a named Engine action.
4443    ///
4444    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
4445    /// For now actions are commit-only: they do not create changelog boundaries and they do not
4446    /// provide rollback/atomicity.
4447    ///
4448    /// Nested actions are deterministically handled by *disallowing* nesting: calling
4449    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
4450    pub fn action<T>(
4451        &mut self,
4452        name: impl AsRef<str>,
4453        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4454    ) -> Result<T, crate::engine::EditorError> {
4455        if self.action_depth != 0 {
4456            return Err(crate::engine::EditorError::TransactionFailed {
4457                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
4458                    .to_string(),
4459            });
4460        }
4461
4462        self.action_depth = 1;
4463        let engine_ptr: *mut Engine<R> = self;
4464        let _guard = ActionDepthGuard {
4465            engine: engine_ptr,
4466            _marker: std::marker::PhantomData,
4467        };
4468
4469        let mut tx = EngineAction {
4470            engine: self,
4471            name: name.as_ref().to_string(),
4472            capture: None,
4473            arrow_undo: None,
4474            atomic_policy: false,
4475        };
4476        f(&mut tx)
4477    }
4478
4479    /// Execute a named Engine action with atomic commit/rollback semantics.
4480    ///
4481    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
4482    pub fn action_atomic<T>(
4483        &mut self,
4484        name: impl Into<String>,
4485        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4486    ) -> Result<T, crate::engine::EditorError> {
4487        let (v, _j) = self.action_atomic_journal(name, f)?;
4488        Ok(v)
4489    }
4490
4491    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
4492    pub fn action_atomic_journal<T>(
4493        &mut self,
4494        name: impl Into<String>,
4495        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4496    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4497        if self.action_depth != 0 {
4498            return Err(crate::engine::EditorError::TransactionFailed {
4499                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4500                    .to_string(),
4501            });
4502        }
4503
4504        self.action_depth = 1;
4505        let engine_ptr: *mut Engine<R> = self;
4506        let _guard = ActionDepthGuard {
4507            engine: engine_ptr,
4508            _marker: std::marker::PhantomData,
4509        };
4510
4511        let name_str = name.into();
4512        let mut capture = MutationCapture::new(Default::default());
4513        let start_len = capture.len();
4514        self.action_atomic_impl(&mut capture, start_len, true, name_str, f)
4515    }
4516
4517    fn action_atomic_impl<T>(
4518        &mut self,
4519        capture: &mut MutationCapture,
4520        start_len: usize,
4521        expand_runs: bool,
4522        name: String,
4523        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4524    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4525        let invalidation_baseline = self.invalidation_baseline();
4526        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
4527        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
4528
4529        let capture_ptr: *mut MutationCapture = capture;
4530        let mut tx = EngineAction {
4531            engine: self,
4532            name: name.clone(),
4533            capture: Some(capture_ptr),
4534            arrow_undo: Some(arrow_ptr),
4535            atomic_policy: true,
4536        };
4537
4538        let res = f(&mut tx);
4539
4540        // Capture graph structural delta for this action. The journal is a
4541        // public value: run records (Program 2) are expanded into it, except
4542        // for a caller that only publishes the capture to its change log
4543        // (`expand_runs` false), where the journal (plain events) drives
4544        // invalidation and records count as topology changes; a rollback
4545        // still replays the expanded events.
4546        let capture_ref = unsafe { &*capture_ptr };
4547        let has_runs = capture_ref.lazy_len() > 0;
4548        let graph_events: Vec<crate::engine::ChangeEvent> = if expand_runs || res.is_err() {
4549            capture_ref.expanded_events_from(start_len, 0)
4550        } else {
4551            capture_ref.events()[start_len..].to_vec()
4552        };
4553        let graph_batch = crate::engine::GraphUndoBatch {
4554            events: graph_events,
4555        };
4556        let affected_cells = arrow_undo.ops.len();
4557        let journal = crate::engine::ActionJournal {
4558            name,
4559            graph: graph_batch,
4560            arrow: arrow_undo,
4561            affected_cells,
4562        };
4563
4564        match res {
4565            Ok(v) => {
4566                if !journal.graph.is_empty() || !journal.arrow.is_empty() || has_runs {
4567                    for event in &journal.graph.events {
4568                        self.record_change_for_event(event);
4569                    }
4570                    let mut impact = Self::classify_change_events(
4571                        &journal.graph.events,
4572                        LoggedEditDirection::Original,
4573                    )
4574                    .max(Self::classify_arrow_undo(&journal.arrow));
4575                    if has_runs {
4576                        impact = impact.max(LoggedEditImpact::Topology);
4577                    }
4578                    self.apply_logged_edit_impact(impact, invalidation_baseline);
4579                }
4580                Ok((v, journal))
4581            }
4582            Err(e) => {
4583                if let Err(rb) = self.rollback_from_action_journal(&journal, invalidation_baseline)
4584                {
4585                    return Err(crate::engine::EditorError::TransactionFailed {
4586                        reason: format!(
4587                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
4588                        ),
4589                    });
4590                }
4591                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
4592                    for event in &journal.graph.events {
4593                        self.record_change_for_event(event);
4594                    }
4595                }
4596                Err(e)
4597            }
4598        }
4599    }
4600
4601    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
4602    ///
4603    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
4604    /// - Dependency graph structural edits (via inverse ChangeEvents)
4605    /// - Arrow-truth overlay writes mirrored from ChangeEvents
4606    /// - ChangeLog entries (published only after a successful commit)
4607    pub fn action_with_logger<T>(
4608        &mut self,
4609        log: &mut crate::engine::ChangeLog,
4610        name: impl AsRef<str>,
4611        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4612    ) -> Result<T, crate::engine::EditorError> {
4613        if self.action_depth != 0 {
4614            return Err(crate::engine::EditorError::TransactionFailed {
4615                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4616                    .to_string(),
4617            });
4618        }
4619
4620        self.action_depth = 1;
4621        let engine_ptr: *mut Engine<R> = self;
4622        let _guard = ActionDepthGuard {
4623            engine: engine_ptr,
4624            _marker: std::marker::PhantomData,
4625        };
4626
4627        let name_str = name.as_ref().to_string();
4628        let mut capture = MutationCapture::new(log.current_meta());
4629        let start_len = capture.len();
4630        capture.begin_compound(name_str.clone());
4631
4632        // Mutation correctness uses the complete private capture. The provided ChangeLog remains
4633        // an observability sink and is not touched until the action outcome is known.
4634        let res = self.action_atomic_impl(&mut capture, start_len, false, name_str, f);
4635        capture.close_compounds();
4636
4637        match res {
4638            Ok((v, _journal)) => {
4639                log.publish_capture(capture);
4640                Ok(v)
4641            }
4642            Err(e) => {
4643                // Preserve sequence/group gaps without retaining failed events or evicting history.
4644                log.discard_capture(capture);
4645                Err(e)
4646            }
4647        }
4648    }
4649
4650    fn rollback_from_action_journal(
4651        &mut self,
4652        journal: &crate::engine::ActionJournal,
4653        invalidation_baseline: InvalidationBaseline,
4654    ) -> Result<(), crate::engine::EditorError> {
4655        // Invalidate first so a partial inverse failure cannot leave a changed
4656        // graph behind an apparently current schedule or lookup cache.
4657        self.invalidate_for_action_journal(
4658            journal,
4659            LoggedEditDirection::InverseReplay,
4660            invalidation_baseline,
4661        );
4662        // 1) Roll back the dependency graph structure.
4663        journal.graph.undo(&mut self.graph)?;
4664        // 2) Roll back engine row-visibility sidecar events.
4665        self.apply_inverse_row_visibility_events(&journal.graph.events);
4666        // 3) Roll back Arrow-truth overlays.
4667        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
4668        Ok(())
4669    }
4670
4671    fn rollback_from_change_events(
4672        &mut self,
4673        events: &[crate::engine::ChangeEvent],
4674        invalidation_baseline: InvalidationBaseline,
4675    ) -> Result<(), crate::engine::EditorError> {
4676        use crate::engine::ChangeEvent;
4677
4678        // Fail closed before applying inverses because replay can return after
4679        // only part of the batch has been restored.
4680        self.invalidate_for_change_events(
4681            events,
4682            LoggedEditDirection::InverseReplay,
4683            invalidation_baseline,
4684        );
4685
4686        // 1) Roll back the dependency graph.
4687        self.graph
4688            .authority_set_replay(crate::engine::authority::history::Replay::Undo);
4689        let rolled_back = (|| {
4690            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
4691            let mut compound_stack: Vec<usize> = Vec::new();
4692            for (i, ev) in events.iter().enumerate().rev() {
4693                match ev {
4694                    ChangeEvent::CompoundEnd { depth } => {
4695                        compound_stack.push(*depth);
4696                        if let Some(description) =
4697                            crate::engine::graph::editor::change_log::compound_start_description(
4698                                i,
4699                                |j| &events[j],
4700                            )
4701                        {
4702                            editor.inverse_compound_end(description);
4703                        }
4704                    }
4705                    ChangeEvent::CompoundStart { depth, .. } => {
4706                        if compound_stack.last() == Some(depth) {
4707                            compound_stack.pop();
4708                        }
4709                        editor.apply_inverse(ev.clone())?;
4710                    }
4711                    ChangeEvent::SetRowVisibility { .. } => {
4712                        // Engine-side metadata handled after dropping graph editor borrow.
4713                    }
4714                    _ => {
4715                        editor.apply_inverse(ev.clone())?;
4716                    }
4717                }
4718            }
4719            Ok::<_, crate::engine::EditorError>(())
4720        })();
4721        self.graph
4722            .authority_set_replay(crate::engine::authority::history::Replay::Forward);
4723        rolled_back?;
4724
4725        // 2) Roll back engine row-visibility metadata.
4726        for ev in events.iter().rev() {
4727            self.apply_inverse_row_visibility_event(ev);
4728        }
4729
4730        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
4731        for ev in events.iter().rev() {
4732            self.mirror_inverse_change_to_arrow(ev);
4733        }
4734
4735        Ok(())
4736    }
4737
4738    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
4739        let sheet_id = self.graph.sheet_id(sheet)?;
4740        let coord = Coord::from_excel(row, col, true, true);
4741        let cell = CellRef::new(sheet_id, coord);
4742        let vid = self.graph.get_vertex_for_cell(&cell)?;
4743        self.graph.get_formula(vid)
4744    }
4745
4746    pub fn define_name_with_logger(
4747        &mut self,
4748        log: &mut crate::engine::ChangeLog,
4749        name: &str,
4750        definition: NamedDefinition,
4751        scope: NameScope,
4752    ) -> Result<(), crate::engine::EditorError> {
4753        self.graph
4754            .validate_define_name(name, scope)
4755            .map_err(crate::engine::EditorError::Excel)?;
4756
4757        {
4758            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4759            editor.define_name(name, definition, scope)?;
4760        }
4761        self.record_structural_change(StructuralScope::AllSheets);
4762
4763        self.mark_topology_edited();
4764
4765        Ok(())
4766    }
4767
4768    pub fn update_name_with_logger(
4769        &mut self,
4770        log: &mut crate::engine::ChangeLog,
4771        name: &str,
4772        definition: NamedDefinition,
4773        scope: NameScope,
4774    ) -> Result<(), crate::engine::EditorError> {
4775        self.graph
4776            .validate_existing_name(name, scope)
4777            .map_err(crate::engine::EditorError::Excel)?;
4778        {
4779            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4780            editor.update_name(name, definition, scope)?;
4781        }
4782        self.record_structural_change(StructuralScope::AllSheets);
4783
4784        self.mark_topology_edited();
4785
4786        Ok(())
4787    }
4788
4789    pub fn delete_name_with_logger(
4790        &mut self,
4791        log: &mut crate::engine::ChangeLog,
4792        name: &str,
4793        scope: NameScope,
4794    ) -> Result<(), crate::engine::EditorError> {
4795        self.graph
4796            .validate_existing_name(name, scope)
4797            .map_err(crate::engine::EditorError::Excel)?;
4798        {
4799            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4800            editor.delete_name(name, scope)?;
4801        }
4802        self.record_structural_change(StructuralScope::AllSheets);
4803
4804        self.mark_topology_edited();
4805
4806        Ok(())
4807    }
4808
4809    pub fn edit_with_logger<T>(
4810        &mut self,
4811        log: &mut crate::engine::ChangeLog,
4812        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
4813    ) -> Result<T, crate::engine::EditorError> {
4814        let mut capture = MutationCapture::new(log.current_meta());
4815        let result = self.edit_with_capture(&mut capture, f);
4816        capture.close_compounds();
4817        match result {
4818            Ok(value) => {
4819                log.publish_capture(capture);
4820                Ok(value)
4821            }
4822            Err(error) => {
4823                log.discard_capture(capture);
4824                Err(error)
4825            }
4826        }
4827    }
4828
4829    fn edit_with_capture<T>(
4830        &mut self,
4831        capture: &mut MutationCapture,
4832        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
4833    ) -> Result<T, crate::engine::EditorError> {
4834        let invalidation_baseline = self.invalidation_baseline();
4835        let start_len = capture.len();
4836        let lazy_start = capture.lazy_len();
4837
4838        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
4839        // (graph value cache is intentionally empty in canonical mode).
4840        struct ArrowSpillReader<'a> {
4841            sheets: &'a crate::arrow_store::SheetStore,
4842        }
4843        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
4844            fn read_cell_value(
4845                &self,
4846                sheet: &str,
4847                row: u32,
4848                col: u32,
4849            ) -> Option<formualizer_common::LiteralValue> {
4850                use formualizer_common::LiteralValue;
4851                let asheet = self.sheets.sheet(sheet)?;
4852                let r0 = row.saturating_sub(1) as usize;
4853                let c0 = col.saturating_sub(1) as usize;
4854                let v = asheet.get_cell_value(r0, c0);
4855                if matches!(v, LiteralValue::Empty) {
4856                    None
4857                } else {
4858                    Some(v)
4859                }
4860            }
4861        }
4862
4863        let ret = {
4864            let spill_reader = ArrowSpillReader {
4865                sheets: &self.arrow_sheets,
4866            };
4867            let mut editor = crate::engine::VertexEditor::with_capture_and_spill_reader(
4868                &mut self.graph,
4869                capture,
4870                &spill_reader,
4871            );
4872            f(&mut editor)
4873        };
4874
4875        // Plain events only: run records (Program 2) stand for
4876        // `FormulaAdjusted` events, which have no forward effect here but
4877        // topology invalidation.
4878        let new_events = capture.events()[start_len..].to_vec();
4879        let new_runs = capture.lazy_len() > lazy_start;
4880        if new_events.iter().any(|event| {
4881            matches!(
4882                event,
4883                ChangeEvent::DefineName { .. }
4884                    | ChangeEvent::UpdateName { .. }
4885                    | ChangeEvent::DeleteName { .. }
4886            )
4887        }) {
4888            let all = capture.expanded_events_from(start_len, lazy_start);
4889            self.rollback_from_change_events(&all, invalidation_baseline)?;
4890            return Err(crate::engine::EditorError::TransactionUnsupported {
4891                reason: "name mutations must use Engine's prepared logged-name APIs".to_string(),
4892            });
4893        }
4894
4895        // Mirror value-impacting graph events to Arrow for forward edits.
4896        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
4897        self.clear_logged_cell_format_states(&new_events);
4898        for ev in &new_events {
4899            self.mirror_forward_change_to_arrow(ev);
4900        }
4901        for ev in &new_events {
4902            self.record_change_for_event(ev);
4903        }
4904
4905        // Atomic EngineAction calls publish one invalidation for their complete
4906        // journal at commit/rollback. Direct logged edits publish here.
4907        if self.action_depth == 0 {
4908            let mut impact =
4909                Self::classify_change_events(&new_events, LoggedEditDirection::Original);
4910            if new_runs {
4911                impact = impact.max(LoggedEditImpact::Topology);
4912            }
4913            self.apply_logged_edit_impact(impact, invalidation_baseline);
4914        }
4915
4916        Ok(ret)
4917    }
4918
4919    pub(crate) fn preflight_replay_admission(
4920        &mut self,
4921        events: &[ChangeEvent],
4922        forward: bool,
4923    ) -> Result<(), crate::engine::EditorError> {
4924        if !self.graph_admission_enabled() {
4925            return Ok(());
4926        }
4927        let mut vertex_delta = 0i128;
4928        let mut edge_delta = 0i128;
4929        let mut added_vertices = 0usize;
4930        let mut added_edges = 0usize;
4931        let mut formula_cells = BTreeSet::new();
4932        for event in events {
4933            match event {
4934                ChangeEvent::AddVertex {
4935                    formula,
4936                    coord,
4937                    sheet_id,
4938                    ..
4939                } => {
4940                    let delta = if forward { 1 } else { -1 };
4941                    vertex_delta += delta;
4942                    if forward {
4943                        added_vertices = added_vertices.saturating_add(1);
4944                        if formula.is_some() {
4945                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
4946                        }
4947                    }
4948                }
4949                ChangeEvent::RemoveVertex {
4950                    old_formula,
4951                    coord,
4952                    sheet_id,
4953                    ..
4954                } => {
4955                    let delta = if forward { -1 } else { 1 };
4956                    vertex_delta += delta;
4957                    if !forward {
4958                        added_vertices = added_vertices.saturating_add(1);
4959                        if old_formula.is_some()
4960                            && let (Some(sheet_id), Some(coord)) = (sheet_id, coord)
4961                        {
4962                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
4963                        }
4964                    }
4965                }
4966                ChangeEvent::EdgeAdded { .. } => {
4967                    let delta = if forward { 1 } else { -1 };
4968                    edge_delta += delta;
4969                    if forward {
4970                        added_edges = added_edges.saturating_add(1);
4971                    }
4972                }
4973                ChangeEvent::EdgeRemoved { .. } => {
4974                    let delta = if forward { -1 } else { 1 };
4975                    edge_delta += delta;
4976                    if !forward {
4977                        added_edges = added_edges.saturating_add(1);
4978                    }
4979                }
4980                ChangeEvent::SetFormula {
4981                    addr, old_formula, ..
4982                } => {
4983                    if forward || old_formula.is_some() {
4984                        formula_cells.insert((addr.sheet_id, addr.coord.row(), addr.coord.col()));
4985                    }
4986                }
4987                _ => {}
4988            }
4989        }
4990        let stats = self.graph.baseline_stats();
4991        let final_vertices = i128::try_from(stats.graph_vertex_count)
4992            .ok()
4993            .and_then(|count| count.checked_add(vertex_delta))
4994            .and_then(|count| usize::try_from(count).ok())
4995            .ok_or_else(|| {
4996                crate::engine::EditorError::Excel(
4997                    ExcelError::new(ExcelErrorKind::NImpl)
4998                        .with_message("replay vertex count overflow"),
4999                )
5000            })?;
5001        let final_edges = i128::try_from(stats.graph_edge_count)
5002            .ok()
5003            .and_then(|count| count.checked_add(edge_delta))
5004            .and_then(|count| usize::try_from(count).ok())
5005            .ok_or_else(|| {
5006                crate::engine::EditorError::Excel(
5007                    ExcelError::new(ExcelErrorKind::NImpl)
5008                        .with_message("replay edge count overflow"),
5009                )
5010            })?;
5011        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
5012            final_vertices,
5013            final_edges,
5014            materialization_cells: formula_cells.len() as u64,
5015            added_vertices,
5016            added_edges,
5017        })
5018        .map_err(crate::engine::EditorError::Excel)
5019    }
5020
5021    /// Undo the last group still retained by the provided audit log.
5022    ///
5023    /// Disabled, zero-cap, and evicted history is unavailable on this index-based path. Use an
5024    /// explicit `ActionJournal` with `undo_action` when undo must be independent of audit retention.
5025    pub fn undo_logged(
5026        &mut self,
5027        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5028        log: &mut crate::engine::ChangeLog,
5029    ) -> Result<(), crate::engine::EditorError> {
5030        let pending_events = log
5031            .last_group_indices()
5032            .into_iter()
5033            .map(|index| log.events()[index].clone())
5034            .collect::<Vec<_>>();
5035        self.preflight_replay_admission(&pending_events, false)?;
5036        let invalidation_baseline = self.invalidation_baseline();
5037        // UndoEngine can fail after partially applying the batch, so publish
5038        // invalidation before replay rather than only on the success path.
5039        self.invalidate_for_change_events(
5040            &pending_events,
5041            LoggedEditDirection::InverseReplay,
5042            invalidation_baseline,
5043        );
5044        let batch = undo.undo(&mut self.graph, log)?;
5045        for item in batch.iter().rev() {
5046            self.apply_inverse_row_visibility_event(&item.event);
5047            self.apply_inverse_staged_formula_event(&item.event);
5048        }
5049        if !batch.is_empty() {
5050            let events = batch
5051                .iter()
5052                .map(|item| item.event.clone())
5053                .collect::<Vec<_>>();
5054            self.clear_logged_cell_format_states(&events);
5055        }
5056        self.mirror_undo_batch_to_arrow(&batch);
5057        if !batch.is_empty() {
5058            for item in &batch {
5059                self.record_change_for_event(&item.event);
5060            }
5061        }
5062        crate::engine::trace::fz_event!(
5063            tracing::Level::INFO,
5064            "history",
5065            "history.replay",
5066            op = "undo",
5067            events_replayed = batch.len(),
5068            ownership_resyncs = batch.len()
5069        );
5070        Ok(())
5071    }
5072
5073    pub fn redo_logged(
5074        &mut self,
5075        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5076        log: &mut crate::engine::ChangeLog,
5077    ) -> Result<(), crate::engine::EditorError> {
5078        let pending_events = undo.pending_redo_events();
5079        self.preflight_replay_admission(&pending_events, true)?;
5080        let invalidation_baseline = self.invalidation_baseline();
5081        self.invalidate_for_change_events(
5082            &pending_events,
5083            LoggedEditDirection::ForwardReplay,
5084            invalidation_baseline,
5085        );
5086        let batch = undo.redo(&mut self.graph, log)?;
5087        for item in &batch {
5088            self.apply_forward_row_visibility_event(&item.event);
5089            self.apply_forward_staged_formula_event(&item.event);
5090        }
5091        if !batch.is_empty() {
5092            let events = batch
5093                .iter()
5094                .map(|item| item.event.clone())
5095                .collect::<Vec<_>>();
5096            self.clear_logged_cell_format_states(&events);
5097        }
5098        self.mirror_redo_batch_to_arrow(&batch);
5099        if !batch.is_empty() {
5100            for item in &batch {
5101                self.record_change_for_event(&item.event);
5102            }
5103        }
5104        crate::engine::trace::fz_event!(
5105            tracing::Level::INFO,
5106            "history",
5107            "history.replay",
5108            op = "redo",
5109            events_replayed = batch.len(),
5110            ownership_resyncs = batch.len()
5111        );
5112        Ok(())
5113    }
5114
5115    /// Undo the last committed atomic action using the journal stack.
5116    ///
5117    /// This path does not require a `ChangeLog`.
5118    pub fn undo_action(
5119        &mut self,
5120        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5121    ) -> Result<(), crate::engine::EditorError> {
5122        let Some(journal) = undo.pop_undo_action() else {
5123            return Ok(());
5124        };
5125        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, false) {
5126            undo.push_done_action(journal);
5127            return Err(error);
5128        }
5129        let invalidation_baseline = self.invalidation_baseline();
5130
5131        self.invalidate_for_action_journal(
5132            &journal,
5133            LoggedEditDirection::InverseReplay,
5134            invalidation_baseline,
5135        );
5136        journal.graph.undo(&mut self.graph)?;
5137        self.apply_inverse_row_visibility_events(&journal.graph.events);
5138        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
5139        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5140            for event in &journal.graph.events {
5141                self.record_change_for_event(event);
5142            }
5143        }
5144
5145        #[cfg(feature = "tracing")]
5146        let events_replayed = journal.graph.events.len();
5147        crate::engine::trace::fz_event!(
5148            tracing::Level::INFO,
5149            "history",
5150            "history.replay",
5151            op = "undo",
5152            events_replayed,
5153            ownership_resyncs = events_replayed
5154        );
5155        undo.push_redo_action(journal);
5156        Ok(())
5157    }
5158
5159    /// Redo the last undone atomic action using the journal stack.
5160    ///
5161    /// This path does not require a `ChangeLog`.
5162    pub fn redo_action(
5163        &mut self,
5164        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5165    ) -> Result<(), crate::engine::EditorError> {
5166        let Some(journal) = undo.pop_redo_action() else {
5167            return Ok(());
5168        };
5169        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, true) {
5170            undo.push_redo_action(journal);
5171            return Err(error);
5172        }
5173        let invalidation_baseline = self.invalidation_baseline();
5174        self.invalidate_for_action_journal(
5175            &journal,
5176            LoggedEditDirection::ForwardReplay,
5177            invalidation_baseline,
5178        );
5179        journal.graph.redo(&mut self.graph)?;
5180        self.apply_forward_row_visibility_events(&journal.graph.events);
5181        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
5182        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5183            for event in &journal.graph.events {
5184                self.record_change_for_event(event);
5185            }
5186        }
5187
5188        #[cfg(feature = "tracing")]
5189        let events_replayed = journal.graph.events.len();
5190        crate::engine::trace::fz_event!(
5191            tracing::Level::INFO,
5192            "history",
5193            "history.replay",
5194            op = "redo",
5195            events_replayed,
5196            ownership_resyncs = events_replayed
5197        );
5198        undo.push_done_action(journal);
5199        Ok(())
5200    }
5201
5202    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
5203        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
5204        // Coord stores 0-based indices.
5205        let row = addr.coord.row() + 1;
5206        let col = addr.coord.col() + 1;
5207        (sheet, row, col)
5208    }
5209
5210    fn mirror_undo_batch_to_arrow(
5211        &mut self,
5212        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5213    ) {
5214        // Undo applies inverses in reverse order.
5215        for item in batch.iter().rev() {
5216            self.mirror_inverse_change_to_arrow(&item.event);
5217        }
5218    }
5219
5220    fn mirror_redo_batch_to_arrow(
5221        &mut self,
5222        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5223    ) {
5224        // Redo applies events in forward order.
5225        for item in batch.iter() {
5226            self.mirror_forward_change_to_arrow(&item.event);
5227        }
5228    }
5229
5230    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5231        use crate::engine::ChangeEvent;
5232        use formualizer_common::LiteralValue;
5233
5234        match ev {
5235            ChangeEvent::SetValue {
5236                addr,
5237                old_value,
5238                old_formula,
5239                ..
5240            } => {
5241                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5242                if old_formula.is_some() {
5243                    self.clear_delta_overlay_cell(&sheet, row, col);
5244                } else {
5245                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5246                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5247                }
5248            }
5249            ChangeEvent::SetFormula {
5250                addr,
5251                old_value,
5252                old_formula,
5253                ..
5254            } => {
5255                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5256                if old_formula.is_some() {
5257                    self.clear_delta_overlay_cell(&sheet, row, col);
5258                } else {
5259                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5260                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5261                }
5262            }
5263            ChangeEvent::SpillCommitted { old, new, .. } => {
5264                // Inverse: restore `old` (or clear if none).
5265                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
5266                if let Some(snap) = old {
5267                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
5268                }
5269            }
5270            ChangeEvent::SpillCleared { old, .. } => {
5271                // Inverse: restore prior spill.
5272                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
5273            }
5274            ChangeEvent::SetRowVisibility { .. } => {
5275                // Engine-side metadata only; no Arrow overlay effect.
5276            }
5277            _ => {}
5278        }
5279    }
5280
5281    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5282        use crate::engine::ChangeEvent;
5283
5284        match ev {
5285            ChangeEvent::SetValue { addr, new, .. } => {
5286                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5287                self.mirror_value_to_overlay(&sheet, row, col, new);
5288            }
5289            ChangeEvent::SetFormula { addr, .. } => {
5290                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5291                self.clear_delta_overlay_cell(&sheet, row, col);
5292                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
5293            }
5294            ChangeEvent::SpillCommitted { old, new, .. } => {
5295                if let Some(snap) = old {
5296                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
5297                }
5298                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
5299            }
5300            ChangeEvent::SpillCleared { old, .. } => {
5301                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
5302            }
5303            ChangeEvent::SetRowVisibility { .. } => {
5304                // Engine-side metadata only; no Arrow overlay effect.
5305            }
5306            _ => {
5307                // Other graph structural operations do not have direct value effects in Arrow.
5308            }
5309        }
5310    }
5311
5312    fn mirror_spill_snapshot(
5313        &mut self,
5314        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
5315        clear_only: bool,
5316    ) {
5317        use formualizer_common::LiteralValue;
5318
5319        let mut i = 0usize;
5320        for row in &snap.values {
5321            for v in row {
5322                if let Some(cell) = snap.target_cells.get(i) {
5323                    let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5324                    let out = if clear_only {
5325                        LiteralValue::Empty
5326                    } else {
5327                        v.clone()
5328                    };
5329                    self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
5330                }
5331                i += 1;
5332            }
5333        }
5334        // If target_cells is longer than values (should not happen), clear remaining cells.
5335        if clear_only {
5336            for cell in snap.target_cells.iter().skip(i) {
5337                let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5338                self.mirror_value_to_computed_overlay(&sheet, r, c, &LiteralValue::Empty);
5339            }
5340        }
5341    }
5342
5343    pub fn set_default_sheet_by_name(&mut self, name: &str) {
5344        self.graph.set_default_sheet_by_name(name);
5345    }
5346
5347    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
5348        self.graph.set_default_sheet_by_id(id);
5349    }
5350
5351    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
5352        self.graph.set_sheet_index_mode(mode);
5353    }
5354
5355    #[cfg(feature = "test-support")]
5356    #[doc(hidden)]
5357    pub fn mark_all_formulas_dirty_for_test(&mut self) {
5358        self.mark_all_formula_vertices_dirty();
5359    }
5360
5361    #[cfg(feature = "test-support")]
5362    #[doc(hidden)]
5363    pub fn take_criteria_mask_work_for_test() -> (usize, usize) {
5364        criteria_mask_test_hooks::take_mask_work()
5365    }
5366
5367    #[cfg(feature = "test-support")]
5368    #[doc(hidden)]
5369    pub fn lookup_index_cache_report_for_test(&self) -> LookupIndexCacheReport {
5370        self.lookup_index_cache.report()
5371    }
5372
5373    #[cfg(any(test, feature = "benchmark_internal"))]
5374    #[doc(hidden)]
5375    pub fn reset_recalc_reuse_probe(&mut self) {
5376        *self.recalc_reuse_probe.get_mut().unwrap() = RecalcReuseProbe::default();
5377    }
5378
5379    #[cfg(any(test, feature = "benchmark_internal"))]
5380    #[doc(hidden)]
5381    pub fn recalc_reuse_probe(&self) -> RecalcReuseProbe {
5382        let mut probe = self.recalc_reuse_probe.lock().unwrap().clone();
5383        if let Some(cached) = self.cached_static_schedule.as_ref() {
5384            let entry_bytes = |e: &CachedScheduleEntry| {
5385                std::mem::size_of::<CachedScheduleEntry>()
5386                    + e.candidate_vertices.heap_bytes()
5387                    + std::mem::size_of::<crate::engine::Schedule>()
5388                    + 2 * std::mem::size_of::<usize>()
5389                    + schedule_probe_retained_bytes(&e.schedule)
5390            };
5391            probe.schedule_retained_bytes = entry_bytes(cached)
5392                + self.recent_schedules.iter().map(entry_bytes).sum::<usize>()
5393                + self.base_schedule.as_ref().map_or(0, entry_bytes)
5394                + self.recent_schedules.capacity() * std::mem::size_of::<CachedScheduleEntry>();
5395        }
5396        probe
5397    }
5398
5399    #[cfg(test)]
5400    pub(crate) fn cached_static_schedule_for_test(&self) -> Option<Arc<crate::engine::Schedule>> {
5401        self.cached_static_schedule
5402            .as_ref()
5403            .map(|cached| Arc::clone(&cached.schedule))
5404    }
5405
5406    fn clear_cached_static_schedule(&mut self) {
5407        self.cached_static_schedule = None;
5408        self.recent_schedules.clear();
5409        self.base_schedule = None;
5410    }
5411
5412    /// Keep a replaced schedule among the recent ones when it is still
5413    /// current and small; drop stale ones and the oldest beyond the bounds.
5414    fn retain_recent_schedule(&mut self, entry: CachedScheduleEntry) {
5415        const RECENT_SCHEDULES: usize = 8;
5416        const RECENT_SCHEDULE_VERTICES: usize = 65_536;
5417        let revision = self.schedule_cache_authority_revision();
5418        let epoch = self.topology_epoch;
5419        self.recent_schedules
5420            .retain(|e| e.topology_epoch == epoch && e.authority_revision == revision);
5421        let current =
5422            |e: &CachedScheduleEntry| e.topology_epoch == epoch && e.authority_revision == revision;
5423        if self.base_schedule.as_ref().is_some_and(|b| !current(b)) {
5424            self.base_schedule = None;
5425        }
5426        // The largest current schedule becomes the base; a replaced base
5427        // may still join the recent ones.
5428        let mut entry = entry;
5429        if current(&entry)
5430            && entry.candidate_vertices.len() > BASE_SCHEDULE_MIN_VERTICES
5431            && self
5432                .base_schedule
5433                .as_ref()
5434                .is_none_or(|b| entry.candidate_vertices.len() > b.candidate_vertices.len())
5435        {
5436            match self.base_schedule.replace(entry) {
5437                Some(previous) => entry = previous,
5438                None => return,
5439            }
5440        }
5441        if entry.topology_epoch != epoch
5442            || entry.authority_revision != revision
5443            || entry.candidate_vertices.len() > RECENT_SCHEDULE_VERTICES / 4
5444        {
5445            return;
5446        }
5447        self.recent_schedules.push(entry);
5448        let mut total: usize = self
5449            .recent_schedules
5450            .iter()
5451            .map(|e| e.candidate_vertices.len())
5452            .sum();
5453        while self.recent_schedules.len() > RECENT_SCHEDULES || total > RECENT_SCHEDULE_VERTICES {
5454            let oldest = self.recent_schedules.remove(0);
5455            total -= oldest.candidate_vertices.len();
5456        }
5457    }
5458
5459    fn invalidation_baseline(&self) -> InvalidationBaseline {
5460        InvalidationBaseline {
5461            snapshot_id: self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed),
5462            topology_epoch: self.topology_epoch,
5463        }
5464    }
5465
5466    fn classify_change_events(
5467        events: &[crate::engine::ChangeEvent],
5468        direction: LoggedEditDirection,
5469    ) -> LoggedEditImpact {
5470        use crate::engine::ChangeEvent;
5471
5472        events
5473            .iter()
5474            .map(|event| match event {
5475                ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
5476                    LoggedEditImpact::NoOp
5477                }
5478                ChangeEvent::SetRowVisibility { .. }
5479                | ChangeEvent::SetValue {
5480                    old_formula: None,
5481                    old_value: Some(_),
5482                    ..
5483                } => LoggedEditImpact::DataOnly,
5484                // Original canonical writes can lack an Arrow old value while
5485                // updating an existing placeholder. Inverse replay actually
5486                // removes that vertex; a later redo recreates it.
5487                ChangeEvent::SetValue {
5488                    old_formula: None,
5489                    old_value: None,
5490                    ..
5491                } if direction == LoggedEditDirection::Original => LoggedEditImpact::DataOnly,
5492                ChangeEvent::SetValue { .. }
5493                | ChangeEvent::SetFormula { .. }
5494                | ChangeEvent::AddVertex { .. }
5495                | ChangeEvent::RemoveVertex { .. }
5496                | ChangeEvent::VertexMoved { .. }
5497                | ChangeEvent::FormulaAdjusted { .. }
5498                | ChangeEvent::NamedRangeAdjusted { .. }
5499                | ChangeEvent::EdgeAdded { .. }
5500                | ChangeEvent::EdgeRemoved { .. }
5501                | ChangeEvent::DefineName { .. }
5502                | ChangeEvent::UpdateName { .. }
5503                | ChangeEvent::DeleteName { .. }
5504                | ChangeEvent::SpillCommitted { .. }
5505                | ChangeEvent::SpillCleared { .. }
5506                | ChangeEvent::StagedFormulaCellChanged { .. } => LoggedEditImpact::Topology,
5507            })
5508            .max()
5509            .unwrap_or(LoggedEditImpact::NoOp)
5510    }
5511
5512    fn classify_arrow_undo(arrow: &crate::engine::ArrowUndoBatch) -> LoggedEditImpact {
5513        use crate::engine::ArrowOp;
5514
5515        arrow
5516            .ops
5517            .iter()
5518            .map(|op| match op {
5519                ArrowOp::SetDeltaCell { .. } | ArrowOp::SetComputedCell { .. } => {
5520                    LoggedEditImpact::DataOnly
5521                }
5522                ArrowOp::RestoreComputedRect { .. }
5523                | ArrowOp::InsertRows { .. }
5524                | ArrowOp::InsertCols { .. } => LoggedEditImpact::Topology,
5525            })
5526            .max()
5527            .unwrap_or(LoggedEditImpact::NoOp)
5528    }
5529
5530    fn apply_logged_edit_impact(
5531        &mut self,
5532        impact: LoggedEditImpact,
5533        baseline: InvalidationBaseline,
5534    ) {
5535        match impact {
5536            LoggedEditImpact::NoOp => {}
5537            LoggedEditImpact::DataOnly => {
5538                if self.topology_epoch == baseline.topology_epoch
5539                    && self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
5540                        == baseline.snapshot_id
5541                {
5542                    self.mark_data_edited();
5543                }
5544            }
5545            LoggedEditImpact::Topology => {
5546                // Some structural entry points already publish topology
5547                // invalidation. Do not bump the same batch twice.
5548                if self.topology_epoch == baseline.topology_epoch {
5549                    self.mark_topology_edited();
5550                }
5551            }
5552        }
5553    }
5554
5555    fn invalidate_for_change_events(
5556        &mut self,
5557        events: &[crate::engine::ChangeEvent],
5558        direction: LoggedEditDirection,
5559        baseline: InvalidationBaseline,
5560    ) {
5561        self.apply_logged_edit_impact(Self::classify_change_events(events, direction), baseline);
5562    }
5563
5564    fn invalidate_for_action_journal(
5565        &mut self,
5566        journal: &crate::engine::ActionJournal,
5567        direction: LoggedEditDirection,
5568        baseline: InvalidationBaseline,
5569    ) {
5570        let impact = Self::classify_change_events(&journal.graph.events, direction)
5571            .max(Self::classify_arrow_undo(&journal.arrow));
5572        self.apply_logged_edit_impact(impact, baseline);
5573    }
5574
5575    /// Mark data edited: bump snapshot and set edited flag.
5576    /// Value-only edits keep the stable-topology schedule cache alive.
5577    pub fn mark_data_edited(&mut self) {
5578        self.lookup_index_cache.clear();
5579        self.snapshot_id
5580            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5581        self.has_edited = true;
5582    }
5583
5584    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
5585    pub fn mark_topology_edited(&mut self) {
5586        self.lookup_index_cache.clear();
5587        self.snapshot_id
5588            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5589        self.topology_epoch = self.topology_epoch.wrapping_add(1);
5590        self.graph.bump_topology_revision();
5591        self.clear_cached_static_schedule();
5592        if let Some(ledger) = self.active_resource_ledger.as_mut() {
5593            let released = ledger.account_mixed_cache(0);
5594            debug_assert!(released.is_ok());
5595        }
5596        self.has_edited = true;
5597        // Eager sync at a topology edit, so read-only (`&self`) plans and
5598        // inspection see a current authority (not during a load or an open
5599        // structural capture).
5600        self.graph.authority_sync_eager();
5601    }
5602
5603    fn mark_all_formula_vertices_dirty(&mut self) {
5604        let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
5605        for vertex in vertices {
5606            self.graph.mark_vertex_dirty(vertex);
5607        }
5608    }
5609
5610    fn mark_moved_formula_vertices_dirty(
5611        &mut self,
5612        summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
5613    ) {
5614        for vertex in &summary.vertices_moved {
5615            if self.graph.has_formula(*vertex) {
5616                self.graph.mark_vertex_dirty(*vertex);
5617            }
5618        }
5619    }
5620
5621    /// Access Arrow sheet store (read-only)
5622    pub fn sheet_store(&self) -> &SheetStore {
5623        &self.arrow_sheets
5624    }
5625
5626    /// True when any sheet carries manual/filter row-visibility state.
5627    /// Used by load-time freshness checks (see `Engine::adopt_file_sheets`).
5628    pub(crate) fn has_row_visibility_state(&self) -> bool {
5629        !self.row_visibility.is_empty()
5630    }
5631
5632    /// Access Arrow sheet store (mutable)
5633    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
5634        &mut self.arrow_sheets
5635    }
5636
5637    pub fn has_staged_formulas(&self) -> bool {
5638        !self.staged_formulas.is_empty()
5639    }
5640
5641    pub fn staged_formula_count(&self) -> usize {
5642        self.staged_formulas.values().map(StagedSheet::len).sum()
5643    }
5644
5645    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
5646    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
5647        self.staged_formulas
5648            .entry(sheet.to_string())
5649            .or_default()
5650            .stage(row, col, text);
5651        self.staged_formula_index.stage(sheet, row, col);
5652        if let Some(sheet) = self.graph.sheet_id(sheet) {
5653            self.invalidate_pending_spills(StructuralScope::Cell {
5654                sheet,
5655                row: row.saturating_sub(1),
5656                col: col.saturating_sub(1),
5657            });
5658        }
5659    }
5660
5661    fn index_removed_staged_sheet(&mut self, sheet: &str, staged: &StagedSheet) {
5662        for (row, col, _) in &staged.entries {
5663            self.staged_formula_index.remove(sheet, *row, *col);
5664        }
5665        if staged.deferred_package.is_some() {
5666            self.staged_formula_index.set_package(sheet, None);
5667        }
5668    }
5669
5670    fn restore_staged_sheet(&mut self, sheet: String, staged: StagedSheet) {
5671        self.staged_formulas.insert(sheet, staged);
5672    }
5673
5674    #[doc(hidden)]
5675    pub fn source_formula_ingress(&mut self) -> SourceFormulaIngress<'_, R> {
5676        SourceFormulaIngress { engine: self }
5677    }
5678
5679    #[doc(hidden)]
5680    /// Test-only fault-injection seam. Not part of the supported API; it exists so
5681    /// integration tests in sibling crates can fail a commit at an exact point.
5682    #[doc(hidden)]
5683    #[cfg(any(test, feature = "test-support"))]
5684    pub fn set_before_prepared_span_commit_hook(
5685        &mut self,
5686        hook: impl FnOnce() + Send + Sync + 'static,
5687    ) {
5688        self.before_prepared_span_commit_hook = Some(Box::new(hook));
5689    }
5690
5691    #[doc(hidden)]
5692    /// Test-only fault-injection seam, matching `set_after_eager_proposal_commit_hook`.
5693    #[cfg(test)]
5694    pub(crate) fn set_before_target_preparation_commit_hook(
5695        &mut self,
5696        hook: impl FnOnce() + Send + Sync + 'static,
5697    ) {
5698        self.before_target_preparation_commit_hook = Some(Box::new(hook));
5699    }
5700
5701    fn stage_deferred_formula_package(&mut self, package: crate::engine::DeferredFormulaPackage) {
5702        if let Ok(replay) = package.replay.lock()
5703            && let Some(footprint) = replay.selection_cache_footprint()
5704            && !self
5705                .source_cache_footprints
5706                .iter()
5707                .any(|known| known.ptr_eq(&footprint))
5708        {
5709            self.source_cache_footprints.push(footprint);
5710        }
5711        let sheet = package.sheet_name.clone();
5712        let staged = self.staged_formulas.entry(sheet.clone()).or_default();
5713        debug_assert!(staged.deferred_package.is_none());
5714        staged.deferred_package = Some(package);
5715        staged.reconcile_attached_deferred_package();
5716        self.staged_formula_index
5717            .set_package(&sheet, staged.deferred_package.as_ref());
5718        if let Some(sheet_id) = self.graph.sheet_id(&sheet) {
5719            let package = self
5720                .staged_formulas
5721                .get(&sheet)
5722                .and_then(|staged| staged.deferred_package.as_ref());
5723            for &(vertex, anchor, region) in &self.blocked_pending_spills {
5724                if anchor.sheet_id == sheet_id
5725                    && self.graph.vertex_exists(vertex)
5726                    && self.graph.get_cell_ref(vertex) == Some(anchor)
5727                    && self.staged_formula_index.package_occupies_spill(
5728                        &sheet,
5729                        (anchor.coord.row() + 1, anchor.coord.col() + 1),
5730                        (
5731                            region.rows.query_bounds().1 + 1,
5732                            region.cols.query_bounds().1 + 1,
5733                        ),
5734                        |point| package.is_some_and(|package| package.suppressed.contains(&point)),
5735                    )
5736                {
5737                    self.graph.mark_vertex_dirty(vertex);
5738                }
5739            }
5740        }
5741    }
5742
5743    pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
5744        let mut removed = None;
5745        let mut remove_sheet = false;
5746        let mut had_package = false;
5747        if let Some(entries) = self.staged_formulas.get_mut(sheet) {
5748            had_package = entries.deferred_package.is_some();
5749            removed = entries.remove(row, col);
5750            remove_sheet = entries.is_empty();
5751        }
5752        let ordinary_removed = self.staged_formula_index.remove(sheet, row, col);
5753        if !ordinary_removed && had_package {
5754            self.staged_formula_index.touch_package(sheet);
5755        }
5756        if remove_sheet {
5757            self.staged_formulas.remove(sheet);
5758            self.staged_formula_index.set_package(sheet, None);
5759        }
5760        if (ordinary_removed || had_package)
5761            && let Some(sheet) = self.graph.sheet_id(sheet)
5762        {
5763            self.invalidate_pending_spills(StructuralScope::Cell {
5764                sheet,
5765                row: row.saturating_sub(1),
5766                col: col.saturating_sub(1),
5767            });
5768        }
5769        removed
5770    }
5771
5772    pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
5773        if self.staged_formulas.remove(sheet).is_some() {
5774            self.staged_formula_index.clear_sheet(sheet);
5775            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
5776                self.invalidate_pending_spills(StructuralScope::Sheet(sheet_id));
5777            }
5778        }
5779    }
5780
5781    pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
5782        let Some(entries) = self.staged_formulas.remove(old) else {
5783            return;
5784        };
5785        self.staged_formula_index.clear_sheet(old);
5786        let (formulas, mut package) = entries.into_parts();
5787        for (row, col, text) in formulas {
5788            self.stage_formula_text(new, row, col, text);
5789        }
5790        if let Some(package) = package.as_mut() {
5791            package.sheet_name = new.to_string();
5792        }
5793        if let Some(package) = package {
5794            self.stage_deferred_formula_package(package);
5795        }
5796    }
5797
5798    /// Get a staged formula text for a given cell if present (cloned).
5799    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
5800        self.staged_formulas
5801            .get(sheet)
5802            .and_then(|v| v.get(row, col))
5803    }
5804
5805    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
5806        &self.formula_parse_diagnostics
5807    }
5808
5809    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
5810        std::mem::take(&mut self.formula_parse_diagnostics)
5811    }
5812
5813    pub fn clear_formula_parse_diagnostics(&mut self) {
5814        self.formula_parse_diagnostics.clear();
5815    }
5816
5817    pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
5818        self.last_formula_ingest_report.as_ref()
5819    }
5820
5821    pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
5822        &self.formula_ingest_report_total
5823    }
5824
5825    #[cfg(test)]
5826    pub(crate) fn set_before_target_planning_snapshot_hook_for_test(
5827        &mut self,
5828        hook: impl FnOnce() + Send + Sync + 'static,
5829    ) {
5830        self.before_target_planning_snapshot_hook = Some(Box::new(hook));
5831    }
5832
5833    #[cfg(test)]
5834    pub(crate) fn inject_target_semantic_stale_once_for_test(&mut self) {
5835        self.inject_target_semantic_stale_once_for_test = true;
5836    }
5837
5838    #[cfg(test)]
5839    pub(crate) fn force_virtual_dep_changes_for_test(&mut self, rounds: usize) {
5840        self.force_virtual_dep_changes_remaining_for_test = rounds;
5841    }
5842
5843    #[cfg(test)]
5844    pub(crate) fn fail_evaluation_commit_preflight_once_for_test(&mut self) {
5845        self.fail_evaluation_commit_preflight_once_for_test = true;
5846    }
5847
5848    #[cfg(test)]
5849    pub(crate) fn set_target_preparation_fault_for_test(
5850        &mut self,
5851        fault: crate::engine::target_preparation::TargetPreparationFault,
5852    ) {
5853        self.target_preparation_fault_for_test = Some(fault);
5854    }
5855
5856    #[cfg(test)]
5857    pub(crate) fn staged_formula_index_revision_for_test(&self) -> u64 {
5858        self.staged_formula_index.revision()
5859    }
5860
5861    #[cfg(test)]
5862    pub(crate) fn deferred_package_for_test(
5863        &self,
5864        sheet: &str,
5865    ) -> &crate::engine::DeferredFormulaPackage {
5866        self.staged_formulas
5867            .get(sheet)
5868            .unwrap()
5869            .deferred_package
5870            .as_ref()
5871            .unwrap()
5872    }
5873
5874    #[cfg(test)]
5875    pub(crate) fn staged_formula_index_is_consistent_for_test(&self) -> bool {
5876        let ordinary_storage = self
5877            .staged_formulas
5878            .values()
5879            .map(|sheet| sheet.entries.len())
5880            .sum::<usize>();
5881        let package_storage = self
5882            .staged_formulas
5883            .values()
5884            .filter(|sheet| sheet.deferred_package.is_some())
5885            .count();
5886        ordinary_storage == self.staged_formula_index.ordinary_count()
5887            && package_storage == self.staged_formula_index.package_count()
5888            && self.staged_formulas.iter().all(|(name, sheet)| {
5889                sheet.entries.iter().all(|(row, col, _)| {
5890                    let leases = self
5891                        .staged_formula_index
5892                        .leases_in_region(name, *row, *col, *row, *col);
5893                    leases.len() == 1 && leases[0].row == *row && leases[0].col == *col
5894                })
5895            })
5896    }
5897
5898    #[cfg(test)]
5899    pub(crate) fn evaluation_request_begin_count_for_test(&self) -> u64 {
5900        self.evaluation_request_begin_count_for_test
5901    }
5902
5903    #[cfg(test)]
5904    pub(crate) fn set_before_legacy_fallback_final_provider_sample_hook(
5905        &mut self,
5906        hook: impl FnOnce() + Send + Sync + 'static,
5907    ) {
5908        self.before_legacy_fallback_final_provider_sample_hook = Some(Box::new(hook));
5909    }
5910
5911    #[cfg(test)]
5912    pub(crate) fn set_after_eager_proposal_commit_hook(
5913        &mut self,
5914        hook: impl FnOnce() + Send + Sync + 'static,
5915    ) {
5916        self.after_eager_proposal_commit_hook = Some(Box::new(hook));
5917    }
5918
5919    #[cfg(test)]
5920    pub(crate) fn topology_epoch_for_test(&self) -> u64 {
5921        self.topology_epoch
5922    }
5923
5924    #[cfg(test)]
5925    pub(crate) fn graph_topology_revision_for_test(&self) -> u64 {
5926        self.graph.topology_revision()
5927    }
5928
5929    fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
5930        self.formula_ingest_report_total.mode = report.mode;
5931        self.formula_ingest_report_total.accumulate(&report);
5932        self.last_formula_ingest_report = Some(report);
5933    }
5934
5935    fn collect_planning_function_requests(
5936        ast: &ASTNode,
5937        requests: &mut Vec<(String, String, usize)>,
5938    ) {
5939        match &ast.node_type {
5940            ASTNodeType::Function { name, args } => {
5941                requests.push((String::new(), name.clone(), args.len()));
5942                for arg in args {
5943                    Self::collect_planning_function_requests(arg, requests);
5944                }
5945            }
5946            ASTNodeType::BinaryOp { left, right, .. } => {
5947                Self::collect_planning_function_requests(left, requests);
5948                Self::collect_planning_function_requests(right, requests);
5949            }
5950            ASTNodeType::UnaryOp { expr, .. } => {
5951                Self::collect_planning_function_requests(expr, requests);
5952            }
5953            ASTNodeType::Call { callee, args } => {
5954                Self::collect_planning_function_requests(callee, requests);
5955                for arg in args {
5956                    Self::collect_planning_function_requests(arg, requests);
5957                }
5958            }
5959            ASTNodeType::Array(rows) => {
5960                for cell in rows.iter().flatten() {
5961                    Self::collect_planning_function_requests(cell, requests);
5962                }
5963            }
5964            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {}
5965        }
5966    }
5967
5968    fn prepared_function_semantics_changed(
5969        &self,
5970        preparation: &crate::engine::FormulaCompressedPreparation,
5971        guard: &crate::function_registry::SemanticEpochReadGuard,
5972    ) -> bool {
5973        if preparation.function_semantic_epoch == guard.epoch() {
5974            return false;
5975        }
5976
5977        guard
5978            .semantic_changes_affect_requests_since(preparation.function_semantic_epoch, Vec::new())
5979    }
5980
5981    pub(crate) fn prepare_source_formula_families(
5982        &mut self,
5983        sheet_name: &str,
5984        families: &[crate::engine::SourceFormulaFamily],
5985    ) -> crate::engine::FormulaCompressedPreparation {
5986        crate::engine::FormulaCompressedPreparation {
5987            engine_token: Arc::clone(&self.source_formula_token),
5988            function_semantic_epoch: crate::function_registry::semantic_epoch(),
5989            function_provider_revision: None,
5990            function_semantics_used: false,
5991            sheet_name: Arc::from(sheet_name),
5992            rejected: BTreeMap::new(),
5993            eager_replay: Vec::new(),
5994            preparation_spool_replays: 0,
5995            clean_rejected_anchor_counts: [0; 3],
5996            fragmented_rejected_anchor_counts: [0; 3],
5997            exact_replay: None,
5998            replay_disposition: crate::engine::FormulaReplayDisposition::default(),
5999        }
6000    }
6001
6002    fn prepare_source_formula_proposals(
6003        &mut self,
6004        sheet_name: &str,
6005        families: &[crate::engine::SourceFormulaFamily],
6006        authority_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6007        replay_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6008        formula_record_count: u64,
6009        replay: Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6010        suppressed: &BTreeSet<(u32, u32)>,
6011        consumed: &FxHashSet<(crate::engine::SourceFamilyId, crate::engine::SourceCoord)>,
6012        consumed_engine: Option<&Arc<()>>,
6013    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
6014        let mut preparation = self.prepare_source_formula_families(sheet_name, families);
6015        preparation.exact_replay = Some(Arc::clone(&replay));
6016        preparation
6017            .replay_disposition
6018            .register_consumed_members(consumed_engine, consumed.iter().copied());
6019        if !preparation
6020            .replay_disposition
6021            .consumed_engine_matches(&self.source_formula_token)
6022        {
6023            return Err(ExcelError::new(ExcelErrorKind::Value)
6024                .with_message("ResidualConsumedEngineMismatch"));
6025        }
6026        preparation
6027            .replay_disposition
6028            .extend_suppressed_excel_coords(suppressed.iter().copied());
6029        Ok(preparation)
6030    }
6031
6032    fn formula_batch_from_exact_replay(
6033        &mut self,
6034        sheet_name: &str,
6035        replayed: impl IntoIterator<Item = crate::engine::DeferredReplayFormula>,
6036    ) -> Result<FormulaIngestBatch, ExcelError> {
6037        let mut cache = rustc_hash::FxHashMap::default();
6038        let mut formulas = Vec::new();
6039        for record in replayed {
6040            let key = if record.text.starts_with('=') {
6041                record.text
6042            } else {
6043                format!("={}", record.text)
6044            };
6045            let ast_id = if let Some(cached) = cache.get(&key) {
6046                *cached
6047            } else {
6048                let parsed = match formualizer_parse::parser::parse(&key) {
6049                    Ok(parsed) => parsed,
6050                    Err(error) => {
6051                        let Some(parsed) = self.handle_formula_parse_error(
6052                            sheet_name,
6053                            record.row,
6054                            record.col,
6055                            &key,
6056                            error.to_string(),
6057                        )?
6058                        else {
6059                            continue;
6060                        };
6061                        parsed
6062                    }
6063                };
6064                let ast_id = self.intern_formula_ast(&parsed);
6065                cache.insert(key.clone(), ast_id);
6066                ast_id
6067            };
6068            formulas.push(
6069                FormulaIngestRecord::new(record.row, record.col, ast_id, Some(Arc::from(key)))
6070                    .with_source_proof(record.source_order, record.family, record.partition_owner),
6071            );
6072        }
6073        Ok(FormulaIngestBatch::new(sheet_name.to_string(), formulas))
6074    }
6075
6076    fn prepare_target_combined_legacy_graph(
6077        &self,
6078        packages: &[PreparedTargetSourcePackage],
6079        ordinary: &[PreparedOrdinaryStagedFormula],
6080    ) -> Result<(PreparedLegacyGraphPlan, usize), ExcelError> {
6081        let mut planned_by_coord = BTreeMap::new();
6082        for package in packages {
6083            for (row, col, ast_id, plan) in &package.legacy {
6084                planned_by_coord.insert((package.sheet_id, *row, *col), (*ast_id, plan.clone()));
6085            }
6086        }
6087        for formula in ordinary {
6088            if let Some((ast_id, plan)) = formula.ast_id.zip(formula.plan.clone()) {
6089                planned_by_coord.insert(
6090                    (formula.sheet_id, formula.lease.row, formula.lease.col),
6091                    (ast_id, plan),
6092                );
6093            }
6094        }
6095        let planned = planned_by_coord
6096            .into_iter()
6097            .map(|((sheet_id, row, col), (ast_id, plan))| (sheet_id, row, col, ast_id, plan))
6098            .collect::<Vec<_>>();
6099        let formula_count = planned.len();
6100        let graph = self
6101            .graph
6102            .prepare_legacy_graph_plan_multi_sheet(planned)
6103            .map_err(|error| {
6104                ExcelError::new(ExcelErrorKind::Value)
6105                    .with_message(format!("target graph preparation failed: {error}"))
6106            })?;
6107        Ok((graph, formula_count))
6108    }
6109
6110    fn replay_target_coordinates(
6111        &mut self,
6112        replay: &Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6113        coordinates: &[(u32, u32)],
6114        deadline: Option<std::time::Instant>,
6115        scratch: &mut u64,
6116    ) -> Result<Option<Vec<crate::engine::DeferredReplayFormula>>, ExcelError> {
6117        if coordinates.is_empty() {
6118            return Ok(Some(Vec::new()));
6119        }
6120        let mut guard = replay.lock().map_err(|_| {
6121            ExcelError::new(ExcelErrorKind::Value)
6122                .with_message("deferred formula spool lock poisoned")
6123        })?;
6124        let retained = guard.selection_cache_footprint().is_some();
6125        let records = guard.replay_selected_exact(coordinates, &mut |work, bytes| {
6126            self.target_preparation_checkpoint(deadline, work)?;
6127            if retained && let Some(ledger) = self.active_resource_ledger.as_mut() {
6128                ledger
6129                    .reserve_retained(bytes)
6130                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
6131                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
6132            }
6133            self.reserve_graph_source_scratch(bytes)?;
6134            *scratch = scratch.saturating_add(bytes);
6135            Ok(())
6136        });
6137        drop(guard);
6138        let reconciled = self.reconcile_source_cache_footprints();
6139        let records = records?;
6140        reconciled?;
6141        Ok(records)
6142    }
6143
6144    fn prepare_target_exact_source_selection(
6145        &mut self,
6146        sheet: &str,
6147        lease: StagedPackageLease,
6148        coordinates: Vec<(u32, u32)>,
6149        previous: &BTreeSet<(u32, u32)>,
6150        allow_partial_shared: bool,
6151        deadline: Option<std::time::Instant>,
6152        scratch: &mut u64,
6153    ) -> Result<Option<PreparedTargetSourcePackage>, ExcelError> {
6154        let package = self
6155            .staged_formulas
6156            .get(sheet)
6157            .and_then(|s| s.deferred_package.as_ref())
6158            .unwrap();
6159        if !package.source_geometry_complete
6160            || ((!package.families.is_empty() || !package.partitioned_families.is_empty())
6161                && !package.coordinates_cover_families)
6162            || package.reconciliation_replay.is_some()
6163        {
6164            return Ok(None);
6165        }
6166        let selected_points: BTreeSet<_> = coordinates
6167            .into_iter()
6168            .filter(|point| !package.suppressed.contains(point))
6169            .collect();
6170        let mut prepared = PreparedTargetSourcePackage::empty_selection(
6171            sheet,
6172            self.graph.sheet_id(sheet).unwrap(),
6173            lease,
6174        );
6175        if selected_points.is_empty() {
6176            return Ok(Some(prepared));
6177        }
6178        let mut routing = crate::engine::FormulaReplayDisposition::default();
6179        for partition in &package.partitioned_families {
6180            routing
6181                .register_partition(partition, false)
6182                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?;
6183        }
6184        routing.extend_suppressed_excel_coords(package.suppressed.iter().copied());
6185        routing.register_consumed_members(
6186            package.consumed_engine.as_ref(),
6187            package.consumed_members.iter().copied(),
6188        );
6189        let replay = Arc::clone(&package.replay);
6190        let source_report = package.accounting_report();
6191        prepared.source_report = source_report;
6192        prepared.disposition = routing.clone();
6193
6194        let replay_points: BTreeSet<_> = selected_points
6195            .iter()
6196            .copied()
6197            .filter(|&(row, col)| !prepared.direct_contains(row, col))
6198            .collect();
6199        let coordinates: Vec<_> = replay_points.iter().copied().collect();
6200        let records = self.replay_target_coordinates(&replay, &coordinates, deadline, scratch)?;
6201        prepared.spool_replays = u64::from(!coordinates.is_empty());
6202        let Some(mut replay_records) = records else {
6203            return Ok(None);
6204        };
6205        for record in &mut replay_records {
6206            if record.family.is_none() {
6207                record.partition_owner = routing
6208                    .ordinary_disposition(crate::engine::SourceCoord {
6209                        row: record.row.saturating_sub(1),
6210                        col: record.col.saturating_sub(1),
6211                    })
6212                    .1;
6213            }
6214        }
6215        replay_records.sort_by_key(|record| record.source_order);
6216        if replay_records
6217            .iter()
6218            .any(|record| !selected_points.contains(&(record.row, record.col)))
6219            || replay_records
6220                .windows(2)
6221                .any(|records| records[0].source_order == records[1].source_order)
6222        {
6223            return Err(ExcelError::new(ExcelErrorKind::Value)
6224                .with_message("invalid indexed exact source selection"));
6225        }
6226        let represented: BTreeSet<_> = replay_records.iter().map(|r| (r.row, r.col)).collect();
6227        if represented != replay_points {
6228            return Err(ExcelError::new(ExcelErrorKind::Value)
6229                .with_message("incomplete indexed exact source selection"));
6230        }
6231        // The last source record must agree with compressed ownership evidence.
6232        // Earlier overridden records retain ordering but demand no dependencies.
6233        let source = self
6234            .staged_formulas
6235            .get(sheet)
6236            .unwrap()
6237            .deferred_package
6238            .as_ref()
6239            .unwrap();
6240        let contains = |rect: crate::engine::SourceRect, coord: crate::engine::SourceCoord| {
6241            coord.row >= rect.start.row
6242                && coord.row <= rect.end.row
6243                && coord.col >= rect.start.col
6244                && coord.col <= rect.end.col
6245        };
6246        let mut checked = BTreeSet::new();
6247        for record in replay_records.iter().rev() {
6248            if !checked.insert((record.row, record.col)) {
6249                continue;
6250            }
6251            let coord = crate::engine::SourceCoord {
6252                row: record.row - 1,
6253                col: record.col - 1,
6254            };
6255            let agrees = |owner, shared: bool| {
6256                record.family == shared.then_some(owner)
6257                    && record.partition_owner.or(record.family) == Some(owner)
6258            };
6259            let mut valid = true;
6260            for family in &source.families {
6261                let owns = match &family.members {
6262                    crate::engine::SourceFamilyMembers::CompleteDomain(domain) => {
6263                        contains(domain.rect(), coord)
6264                    }
6265                    crate::engine::SourceFamilyMembers::ExplicitMembers(members) => {
6266                        members.as_slice().binary_search(&coord).is_ok()
6267                    }
6268                };
6269                if owns && !agrees(family.source_id, true) {
6270                    valid = false;
6271                }
6272            }
6273            for family in &source.partitioned_families {
6274                if family
6275                    .fragments
6276                    .iter()
6277                    .any(|fragment| contains(fragment.rect(), coord))
6278                    && !agrees(family.source_id, true)
6279                {
6280                    valid = false;
6281                }
6282                if let Some(member) = family
6283                    .legacy_members
6284                    .as_slice()
6285                    .iter()
6286                    .find(|member| member.coord == coord)
6287                    && !agrees(
6288                        family.source_id,
6289                        member.kind == crate::engine::PartitionLegacyMemberKind::SharedFamilyMember,
6290                    )
6291                {
6292                    valid = false;
6293                }
6294            }
6295            if !valid {
6296                return Err(ExcelError::new(ExcelErrorKind::Value)
6297                    .with_message("indexed source ownership mismatch"));
6298            }
6299        }
6300        prepared.selected_points = Some(selected_points);
6301        prepared.replay_records = replay_records;
6302        Ok(Some(prepared))
6303    }
6304
6305    fn prepare_target_source_package(
6306        &mut self,
6307        sheet: &str,
6308        lease: StagedPackageLease,
6309        deadline: Option<std::time::Instant>,
6310    ) -> Result<PreparedTargetSourcePackage, ExcelError> {
6311        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
6312            ExcelError::new(ExcelErrorKind::Ref)
6313                .with_message(format!("deferred source sheet not found: {sheet}"))
6314        })?;
6315        let (
6316            source_report,
6317            families,
6318            partitions,
6319            replay,
6320            invalidated,
6321            suppressed,
6322            consumed_members,
6323            consumed_engine,
6324            reconciliation_replay,
6325        ) = {
6326            let package = self
6327                .staged_formulas
6328                .get(sheet)
6329                .and_then(|staged| staged.deferred_package.as_ref())
6330                .ok_or_else(|| {
6331                    ExcelError::new(ExcelErrorKind::Value)
6332                        .with_message("staged deferred source package is unavailable")
6333                })?;
6334            if package.sheet_name != sheet {
6335                return Err(ExcelError::new(ExcelErrorKind::Value)
6336                    .with_message("deferred formula package sheet mismatch"));
6337            }
6338            (
6339                package.accounting_report(),
6340                package.families.clone(),
6341                package.partitioned_families.clone(),
6342                Arc::clone(&package.replay),
6343                package.invalidated.clone(),
6344                package.suppressed.clone(),
6345                package.consumed_members.clone(),
6346                package.consumed_engine.clone(),
6347                package.reconciliation_replay.clone(),
6348            )
6349        };
6350
6351        let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
6352        for partition in &partitions {
6353            replay_disposition
6354                .register_partition(partition, false)
6355                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
6356        }
6357        replay_disposition.extend_suppressed_excel_coords(suppressed.iter().copied());
6358        replay_disposition
6359            .register_consumed_members(consumed_engine.as_ref(), consumed_members.iter().copied());
6360        if !replay_disposition.consumed_engine_matches(&self.source_formula_token) {
6361            return Err(ExcelError::new(ExcelErrorKind::Value)
6362                .with_message("ResidualConsumedEngineMismatch"));
6363        }
6364        self.target_preparation_checkpoint(deadline, 1)?;
6365        let mut replay_records = if let Some(mut records) = reconciliation_replay {
6366            records.retain(|record| {
6367                let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1))
6368                else {
6369                    return true;
6370                };
6371                let coord = crate::engine::SourceCoord { row, col };
6372                let disposition = record.family.map_or_else(
6373                    || replay_disposition.ordinary_disposition(coord).0,
6374                    |family| replay_disposition.shared_disposition(family, coord),
6375                );
6376                !matches!(
6377                    disposition,
6378                    crate::engine::FormulaReplayCoordinateDisposition::Direct
6379                        | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
6380                )
6381            });
6382            records
6383        } else {
6384            replay
6385                .lock()
6386                .map_err(|_| {
6387                    ExcelError::new(ExcelErrorKind::Value)
6388                        .with_message("deferred formula spool lock poisoned")
6389                })?
6390                .replay_partitioned(&replay_disposition, &partitions)
6391                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?
6392        };
6393        replay_records.sort_by_key(|record| record.source_order);
6394        for chunk in replay_records.chunks(256) {
6395            self.target_preparation_checkpoint(deadline, chunk.len() as u64)?;
6396        }
6397        if replay_records
6398            .windows(2)
6399            .any(|records| records[0].source_order == records[1].source_order)
6400        {
6401            return Err(ExcelError::new(ExcelErrorKind::Value)
6402                .with_message("duplicate deferred source-order proof"));
6403        }
6404
6405        let disposition = replay_disposition;
6406        let direct_families = 0usize;
6407        let direct_cells = 0u64;
6408        let direct_fragments = 0u64;
6409        let direct_complete_families = 0u64;
6410        let direct_complete_cells = 0u64;
6411        let direct_partition_families = 0u64;
6412        let direct_partition_cells = 0u64;
6413        let anchor_parses = 0u64;
6414        let anchor_asts = 0u64;
6415        let anchor_analyses = 0u64;
6416
6417        Ok(PreparedTargetSourcePackage {
6418            sheet: sheet.to_string(),
6419            sheet_id,
6420            lease,
6421            selected_points: None,
6422            complete_selections: Default::default(),
6423            deferred_shared: false,
6424            direct_domains: Vec::new(),
6425            source_report,
6426            replay_records,
6427            spool_replays: 1,
6428            disposition,
6429            legacy: Vec::new(),
6430            direct_families,
6431            direct_cells,
6432            direct_fragments,
6433            direct_complete_families,
6434            direct_complete_cells,
6435            direct_partition_families,
6436            direct_partition_cells,
6437            anchor_parses,
6438            anchor_asts,
6439            anchor_analyses,
6440        })
6441    }
6442
6443    fn fallback_planning_snapshot(
6444        &self,
6445        batch: &FormulaIngestBatch,
6446    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
6447        let mut requests = Vec::new();
6448        for formula in &batch.formulas {
6449            let ast = self
6450                .graph
6451                .data_store()
6452                .retrieve_ast(formula.ast_id, self.graph.sheet_reg())
6453                .ok_or_else(|| {
6454                    ExcelError::new(ExcelErrorKind::Value)
6455                        .with_message("ordered fallback AST is unavailable")
6456                })?;
6457            Self::collect_planning_function_requests(&ast, &mut requests);
6458        }
6459        requests.sort();
6460        requests.dedup();
6461        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
6462            &self.resolver,
6463            requests,
6464        )
6465        .map_err(|error| ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}")))
6466    }
6467
6468    fn prepare_legacy_batch_fallback(
6469        &mut self,
6470        batch: FormulaIngestBatch,
6471        function_provider: &dyn crate::traits::FunctionProvider,
6472    ) -> Result<
6473        (
6474            crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan,
6475            u64,
6476        ),
6477        ExcelError,
6478    > {
6479        let formula_count = batch.formulas.len() as u64;
6480        let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
6481        let mut planned = Vec::with_capacity(batch.formulas.len());
6482        for record in batch.formulas {
6483            let placement = CellRef::new(
6484                sheet_id,
6485                Coord::from_excel(record.row, record.col, true, true),
6486            );
6487            let ingested = self
6488                .graph
6489                .ingest_pipeline(function_provider)
6490                .enable_function_semantics()
6491                .ingest_formula(
6492                    FormulaAstInput::RawArena(record.ast_id),
6493                    placement,
6494                    record.formula_text,
6495                )
6496                .map_err(|error| {
6497                    ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}"))
6498                })?;
6499            planned.push((record.row, record.col, ingested.ast_id, ingested.dep_plan));
6500        }
6501        let plan = self
6502            .graph
6503            .prepare_legacy_graph_plan(sheet_id, planned)
6504            .map_err(|error| {
6505                ExcelError::new(ExcelErrorKind::Value).with_message(error.to_string())
6506            })?;
6507        Ok((plan, formula_count))
6508    }
6509
6510    fn publish_compressed_partial_report(
6511        &mut self,
6512        report: &FormulaIngestReport,
6513        direct_report: &FormulaIngestReport,
6514    ) {
6515        if direct_report.source_family_promoted == 0
6516            && direct_report.graph_formula_cells_materialized == 0
6517        {
6518            return;
6519        }
6520        let mut published = report.clone();
6521        published.accumulate(direct_report);
6522        self.record_formula_ingest_report(published);
6523    }
6524
6525    fn finish_compressed_formula_sources(
6526        &mut self,
6527        batches: Vec<(
6528            FormulaIngestBatch,
6529            crate::engine::FormulaCompressedSourceReport,
6530            crate::engine::FormulaCompressedPreparation,
6531        )>,
6532    ) -> Result<FormulaIngestReport, ExcelError> {
6533        self.observe_function_semantic_epoch()?;
6534        if batches.iter().any(|(_, _, preparation)| {
6535            !Arc::ptr_eq(&preparation.engine_token, &self.source_formula_token)
6536        }) {
6537            return Err(ExcelError::new(ExcelErrorKind::Value)
6538                .with_message("compressed source preparation belongs to another engine"));
6539        }
6540        if batches.iter().any(|(fallback, _, preparation)| {
6541            preparation.sheet_name.as_ref() != fallback.sheet_name
6542        }) {
6543            return Err(ExcelError::new(ExcelErrorKind::Value)
6544                .with_message("compressed source preparation sheet mismatch"));
6545        }
6546        let initial_guard = crate::function_registry::semantic_epoch_read_guard();
6547        let initial_provider_revision = self.resolver.planning_semantic_revision();
6548        let mut fallback_batches = Vec::with_capacity(batches.len());
6549        let mut stale_fallback_batches = Vec::new();
6550        let mut pending_preparations = Vec::new();
6551        for (mut fallback, mut source, mut preparation) in batches {
6552            for formula in fallback.formulas.drain(..) {
6553                let source_order = formula.source_order.ok_or_else(|| {
6554                    ExcelError::new(ExcelErrorKind::Value).with_message(
6555                        "compressed source supplied formulas without source-order proof",
6556                    )
6557                })?;
6558                let text = formula.formula_text.ok_or_else(|| {
6559                    ExcelError::new(ExcelErrorKind::Value).with_message(
6560                        "ordered compressed fallback formula has no exact source text",
6561                    )
6562                })?;
6563                preparation
6564                    .eager_replay
6565                    .push(crate::engine::DeferredReplayFormula {
6566                        source_order,
6567                        row: formula.row,
6568                        col: formula.col,
6569                        text: text.to_string(),
6570                        family: formula.source_family,
6571                        partition_owner: formula.partition_owner,
6572                    });
6573            }
6574            preparation
6575                .eager_replay
6576                .sort_by_key(|record| record.source_order);
6577            source.source_spool_replays = source
6578                .source_spool_replays
6579                .saturating_add(preparation.preparation_spool_replays);
6580            let stale_reason = preparation
6581                .function_semantics_used
6582                .then(|| {
6583                    if preparation.function_provider_revision != initial_provider_revision {
6584                        Some("FunctionProviderRevisionChanged")
6585                    } else if self.prepared_function_semantics_changed(&preparation, &initial_guard)
6586                    {
6587                        Some("FunctionSemanticEpochChanged")
6588                    } else {
6589                        None
6590                    }
6591                })
6592                .flatten();
6593            let stale_semantics = stale_reason.is_some();
6594            for reason in preparation.rejected.values() {
6595                *source.fallback_reasons.entry(reason.clone()).or_default() += 1;
6596            }
6597            // No family is ever placed directly: every family replays.
6598            source.replay_families = source.families_seen;
6599            source.replay_cells = source.family_cells_seen;
6600            let compressed = crate::engine::FormulaCompressedSourceBatch::new(
6601                fallback.sheet_name.clone(),
6602                source,
6603            );
6604            if stale_semantics {
6605                stale_fallback_batches.push((fallback, compressed));
6606            } else {
6607                fallback_batches.push((fallback, compressed));
6608            }
6609            pending_preparations.push((preparation, stale_semantics));
6610        }
6611        drop(initial_guard);
6612
6613        // Build known fallback graphs first. Stale batches are forced through legacy ingest.
6614        let configured_mode = self.config.formula_plane_mode;
6615        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6616        let stale_result =
6617            self.ingest_compressed_formula_source_batches_inner(stale_fallback_batches, false);
6618        self.config.formula_plane_mode = configured_mode;
6619        let mut report = stale_result?;
6620        report.mode = configured_mode;
6621
6622        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6623        let fallback_result =
6624            self.ingest_compressed_formula_source_batches_inner(fallback_batches, false);
6625        self.config.formula_plane_mode = configured_mode;
6626        match fallback_result {
6627            Ok(fallback_report) => report.accumulate(&fallback_report),
6628            Err(error) => {
6629                self.record_formula_ingest_report(report);
6630                return Err(error);
6631            }
6632        }
6633
6634        let mut direct_report =
6635            FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
6636        for (preparation, _) in &pending_preparations {
6637            direct_report.source_anchor_parses = direct_report.source_anchor_parses.saturating_add(
6638                preparation.clean_rejected_anchor_counts[0]
6639                    .saturating_add(preparation.fragmented_rejected_anchor_counts[0]),
6640            );
6641            direct_report.source_anchor_asts = direct_report.source_anchor_asts.saturating_add(
6642                preparation.clean_rejected_anchor_counts[1]
6643                    .saturating_add(preparation.fragmented_rejected_anchor_counts[1]),
6644            );
6645            direct_report.source_anchor_analyses =
6646                direct_report.source_anchor_analyses.saturating_add(
6647                    preparation.clean_rejected_anchor_counts[2]
6648                        .saturating_add(preparation.fragmented_rejected_anchor_counts[2]),
6649                );
6650        }
6651        loop {
6652            #[cfg(any(test, feature = "test-support"))]
6653            if let Some(hook) = self.before_prepared_span_commit_hook.take() {
6654                hook();
6655            }
6656            let commit_guard = crate::function_registry::semantic_epoch_read_guard();
6657            let commit_provider_revision = self.resolver.planning_semantic_revision();
6658            let mut newly_stale = Vec::new();
6659            let mut current = Vec::new();
6660            for pending in pending_preparations.drain(..) {
6661                let stale_reason = pending
6662                    .0
6663                    .function_semantics_used
6664                    .then(|| {
6665                        if pending.0.function_provider_revision != commit_provider_revision {
6666                            Some("FunctionProviderRevisionChanged")
6667                        } else if self
6668                            .prepared_function_semantics_changed(&pending.0, &commit_guard)
6669                        {
6670                            Some("FunctionSemanticEpochChanged")
6671                        } else {
6672                            None
6673                        }
6674                    })
6675                    .flatten();
6676                if let Some(reason) = stale_reason {
6677                    newly_stale.push((pending, reason));
6678                } else {
6679                    current.push(pending);
6680                }
6681            }
6682            if !newly_stale.is_empty() {
6683                drop(commit_guard);
6684                for ((mut preparation, was_initially_stale), reason) in newly_stale {
6685                    preparation
6686                        .eager_replay
6687                        .sort_by_key(|record| record.source_order);
6688                    direct_report.source_spool_replays =
6689                        direct_report.source_spool_replays.saturating_add(1);
6690                    if !was_initially_stale {
6691                        direct_report
6692                            .fallback_reasons
6693                            .entry(reason.to_string())
6694                            .or_default();
6695                    }
6696                    preparation.function_semantics_used = false;
6697                    if !preparation.eager_replay.is_empty() {
6698                        current.push((preparation, false));
6699                    }
6700                }
6701                pending_preparations = current;
6702                continue;
6703            }
6704            drop(commit_guard);
6705
6706            // Replay fallback families in source order.
6707            enum SourceProposal {
6708                KnownFallback {
6709                    source_order: crate::engine::SourceFormulaOrder,
6710                    records: Vec<crate::engine::DeferredReplayFormula>,
6711                },
6712            }
6713
6714            impl SourceProposal {
6715                fn source_order(&self) -> crate::engine::SourceFormulaOrder {
6716                    match self {
6717                        Self::KnownFallback { source_order, .. } => *source_order,
6718                    }
6719                }
6720            }
6721
6722            for (mut preparation, _) in current {
6723                let mut proposals = Vec::with_capacity(preparation.eager_replay.len());
6724                let mut family_fallbacks: BTreeMap<_, Vec<_>> = BTreeMap::new();
6725                for record in preparation.eager_replay.drain(..) {
6726                    if let Some(owner) = record.partition_owner.or(record.family) {
6727                        family_fallbacks.entry(owner).or_default().push(record);
6728                    } else {
6729                        proposals.push(SourceProposal::KnownFallback {
6730                            source_order: record.source_order,
6731                            records: vec![record],
6732                        });
6733                    }
6734                }
6735                for (_, mut records) in family_fallbacks {
6736                    records.sort_by_key(|record| record.source_order);
6737                    if records
6738                        .windows(2)
6739                        .any(|window| window[0].source_order == window[1].source_order)
6740                    {
6741                        self.publish_compressed_partial_report(&report, &direct_report);
6742                        return Err(ExcelError::new(ExcelErrorKind::Value)
6743                            .with_message("duplicate exact-replay source-order proof"));
6744                    }
6745                    let Some(source_order) = records.first().map(|record| record.source_order)
6746                    else {
6747                        self.publish_compressed_partial_report(&report, &direct_report);
6748                        return Err(ExcelError::new(ExcelErrorKind::Value)
6749                            .with_message("empty exact-replay fallback family"));
6750                    };
6751                    proposals.push(SourceProposal::KnownFallback {
6752                        source_order,
6753                        records,
6754                    });
6755                }
6756                proposals.sort_by_key(SourceProposal::source_order);
6757                if proposals
6758                    .windows(2)
6759                    .any(|window| window[0].source_order() == window[1].source_order())
6760                {
6761                    self.publish_compressed_partial_report(&report, &direct_report);
6762                    return Err(ExcelError::new(ExcelErrorKind::Value)
6763                        .with_message("ambiguous compressed source-order proof"));
6764                }
6765
6766                for proposal in proposals {
6767                    match proposal {
6768                        SourceProposal::KnownFallback { records, .. } => {
6769                            let batch = match self.formula_batch_from_exact_replay(
6770                                preparation.sheet_name.as_ref(),
6771                                records,
6772                            ) {
6773                                Ok(batch) => batch,
6774                                Err(error) => {
6775                                    self.publish_compressed_partial_report(&report, &direct_report);
6776                                    return Err(error);
6777                                }
6778                            };
6779                            if batch.is_empty() {
6780                                continue;
6781                            }
6782                            let snapshot = match self.fallback_planning_snapshot(&batch) {
6783                                Ok(snapshot) => snapshot,
6784                                Err(error) => {
6785                                    self.publish_compressed_partial_report(&report, &direct_report);
6786                                    return Err(error);
6787                                }
6788                            };
6789                            let commit_guard =
6790                                crate::function_registry::semantic_epoch_read_guard();
6791                            let provider_revision_initial =
6792                                self.resolver.planning_semantic_revision();
6793                            if (commit_guard.epoch() != snapshot.epoch()
6794                                && snapshot.semantic_changes_affect_requests_since_guarded(
6795                                    &commit_guard,
6796                                    snapshot.epoch(),
6797                                ))
6798                                || snapshot.provider_revision().is_some_and(|revision| {
6799                                    Some(revision) != provider_revision_initial
6800                                })
6801                            {
6802                                drop(commit_guard);
6803                                self.publish_compressed_partial_report(&report, &direct_report);
6804                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6805                                    "ordered fallback planning snapshot became stale",
6806                                ));
6807                            }
6808                            let (plan, formula_count) = match self
6809                                .prepare_legacy_batch_fallback(batch, &snapshot)
6810                            {
6811                                Ok(plan) => plan,
6812                                Err(error) => {
6813                                    drop(commit_guard);
6814                                    self.publish_compressed_partial_report(&report, &direct_report);
6815                                    return Err(error);
6816                                }
6817                            };
6818                            let provider_revision_after =
6819                                self.resolver.planning_semantic_revision();
6820                            if provider_revision_after != provider_revision_initial {
6821                                drop(commit_guard);
6822                                self.publish_compressed_partial_report(&report, &direct_report);
6823                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6824                                    "function provider changed while preparing ordered fallback",
6825                                ));
6826                            }
6827                            let graph_vertices = plan.new_vertex_count();
6828                            let Some(graph_edges) = plan.planned_edge_count() else {
6829                                drop(commit_guard);
6830                                self.publish_compressed_partial_report(&report, &direct_report);
6831                                return Err(ExcelError::new(ExcelErrorKind::Value)
6832                                    .with_message("prepared ordered fallback size overflow"));
6833                            };
6834                            if let Err(error) = self.prepared_legacy_admission(&plan, formula_count)
6835                            {
6836                                drop(commit_guard);
6837                                self.publish_compressed_partial_report(&report, &direct_report);
6838                                return Err(error);
6839                            }
6840                            if let Err(error) =
6841                                self.graph.validate_prepared_legacy_graph_plan(&plan)
6842                            {
6843                                drop(commit_guard);
6844                                self.publish_compressed_partial_report(&report, &direct_report);
6845                                return Err(ExcelError::new(ExcelErrorKind::Value)
6846                                    .with_message(error.to_string()));
6847                            }
6848                            #[cfg(test)]
6849                            if let Some(hook) = self
6850                                .before_legacy_fallback_final_provider_sample_hook
6851                                .take()
6852                            {
6853                                hook();
6854                            }
6855                            let provider_revision_final =
6856                                self.resolver.planning_semantic_revision();
6857                            if provider_revision_final != provider_revision_initial {
6858                                drop(commit_guard);
6859                                self.publish_compressed_partial_report(&report, &direct_report);
6860                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6861                                    "function provider changed after ordered fallback validation",
6862                                ));
6863                            }
6864                            let graph_formulas =
6865                                self.graph.apply_prevalidated_legacy_graph_plan(plan);
6866                            direct_report.formula_cells_seen = direct_report
6867                                .formula_cells_seen
6868                                .saturating_add(formula_count);
6869                            direct_report.graph_formula_cells_materialized = direct_report
6870                                .graph_formula_cells_materialized
6871                                .saturating_add(graph_formulas as u64);
6872                            direct_report.graph_vertices_created = direct_report
6873                                .graph_vertices_created
6874                                .saturating_add(graph_vertices as u64);
6875                            direct_report.graph_edges_created = direct_report
6876                                .graph_edges_created
6877                                .saturating_add(graph_edges as u64);
6878                        }
6879                    }
6880                    #[cfg(test)]
6881                    if let Some(hook) = self.after_eager_proposal_commit_hook.take() {
6882                        hook();
6883                    }
6884                }
6885            }
6886            break;
6887        }
6888        report.accumulate(&direct_report);
6889        self.record_formula_ingest_report(report.clone());
6890        Ok(report)
6891    }
6892    /// Ingest replayed per-cell formulas while preserving compressed source counters.
6893    pub(crate) fn ingest_compressed_formula_source_batches(
6894        &mut self,
6895        batches: Vec<(
6896            FormulaIngestBatch,
6897            crate::engine::FormulaCompressedSourceBatch,
6898        )>,
6899    ) -> Result<FormulaIngestReport, ExcelError> {
6900        self.ingest_compressed_formula_source_batches_inner(batches, true)
6901    }
6902
6903    fn ingest_compressed_formula_source_batches_inner(
6904        &mut self,
6905        batches: Vec<(
6906            FormulaIngestBatch,
6907            crate::engine::FormulaCompressedSourceBatch,
6908        )>,
6909        publish_report: bool,
6910    ) -> Result<FormulaIngestReport, ExcelError> {
6911        let mut source_counts = [0_u64; 11];
6912        let mut source_report = crate::engine::FormulaCompressedSourceReport::default();
6913        let mut formula_batches = Vec::with_capacity(batches.len());
6914        let mut compressed_families = Vec::new();
6915        let mut partitioned_families = Vec::new();
6916        for (batch, compressed_batch) in batches {
6917            let (sheet_name, compressed, families, partitions) = compressed_batch.into_parts();
6918            if sheet_name.as_ref() != batch.sheet_name {
6919                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6920                    "compressed formula source sheet does not match its replay batch",
6921                ));
6922            }
6923            compressed_families.push((batch.sheet_name.clone(), families));
6924            partitioned_families.push((batch.sheet_name.clone(), partitions));
6925            source_counts[0] = source_counts[0].saturating_add(compressed.source_formula_events);
6926            source_counts[1] = source_counts[1].saturating_add(compressed.source_ordinary_events);
6927            source_counts[2] =
6928                source_counts[2].saturating_add(compressed.source_shared_anchor_events);
6929            source_counts[3] =
6930                source_counts[3].saturating_add(compressed.source_shared_descendant_events);
6931            source_counts[4] = source_counts[4].saturating_add(compressed.source_unknown_events);
6932            source_counts[5] =
6933                source_counts[5].saturating_add(compressed.source_formula_records_spooled);
6934            source_counts[6] =
6935                source_counts[6].saturating_add(compressed.source_spool_encoded_bytes);
6936            source_counts[7] = source_counts[7].max(compressed.source_spool_peak_memory_bytes);
6937            source_counts[8] =
6938                source_counts[8].saturating_add(compressed.source_spool_spilled_bytes);
6939            source_counts[9] = source_counts[9].saturating_add(compressed.source_spool_spill_files);
6940            source_counts[10] = source_counts[10].saturating_add(compressed.source_spool_replays);
6941            source_report.families_seen = source_report
6942                .families_seen
6943                .saturating_add(compressed.families_seen);
6944            source_report.family_cells_seen = source_report
6945                .family_cells_seen
6946                .saturating_add(compressed.family_cells_seen);
6947            source_report.source_clean_families = source_report
6948                .source_clean_families
6949                .saturating_add(compressed.source_clean_families);
6950            source_report.source_clean_cells = source_report
6951                .source_clean_cells
6952                .saturating_add(compressed.source_clean_cells);
6953            source_report.source_fragmentable_families = source_report
6954                .source_fragmentable_families
6955                .saturating_add(compressed.source_fragmentable_families);
6956            source_report.source_fragmentable_cells = source_report
6957                .source_fragmentable_cells
6958                .saturating_add(compressed.source_fragmentable_cells);
6959            source_report.source_fragment_count = source_report
6960                .source_fragment_count
6961                .saturating_add(compressed.source_fragment_count);
6962            source_report.source_isolated_fallback_cells = source_report
6963                .source_isolated_fallback_cells
6964                .saturating_add(compressed.source_isolated_fallback_cells);
6965            source_report.source_hole_exclusions = source_report
6966                .source_hole_exclusions
6967                .saturating_add(compressed.source_hole_exclusions);
6968            source_report.source_ordinary_exclusions = source_report
6969                .source_ordinary_exclusions
6970                .saturating_add(compressed.source_ordinary_exclusions);
6971            source_report.source_partition_failures = source_report
6972                .source_partition_failures
6973                .saturating_add(compressed.source_partition_failures);
6974            source_report.replay_families = source_report
6975                .replay_families
6976                .saturating_add(compressed.replay_families);
6977            source_report.replay_cells = source_report
6978                .replay_cells
6979                .saturating_add(compressed.replay_cells);
6980            source_report.forward_descendants = source_report
6981                .forward_descendants
6982                .saturating_add(compressed.forward_descendants);
6983            source_report.evidence_limit_fallbacks = source_report
6984                .evidence_limit_fallbacks
6985                .saturating_add(compressed.evidence_limit_fallbacks);
6986            source_report.evidence_peak_bytes = source_report
6987                .evidence_peak_bytes
6988                .max(compressed.evidence_peak_bytes);
6989            for (reason, count) in compressed.fallback_reasons {
6990                *source_report.fallback_reasons.entry(reason).or_default() += count;
6991            }
6992            formula_batches.push(batch);
6993        }
6994        self.ingest_formula_batches_inner(
6995            formula_batches,
6996            source_counts,
6997            Some(source_report),
6998            compressed_families,
6999            partitioned_families,
7000            publish_report,
7001        )
7002    }
7003
7004    pub fn ingest_formula_batches(
7005        &mut self,
7006        batches: Vec<FormulaIngestBatch>,
7007    ) -> Result<FormulaIngestReport, ExcelError> {
7008        let has_formulas = batches.iter().any(|batch| !batch.formulas.is_empty());
7009        let report = self.ingest_formula_batches_inner(
7010            batches,
7011            [0; 11],
7012            None,
7013            Vec::new(),
7014            Vec::new(),
7015            true,
7016        )?;
7017        if has_formulas {
7018            self.mark_topology_edited();
7019        }
7020        Ok(report)
7021    }
7022
7023    fn ingest_formula_batches_unpublished(
7024        &mut self,
7025        batches: Vec<FormulaIngestBatch>,
7026    ) -> Result<FormulaIngestReport, ExcelError> {
7027        self.ingest_formula_batches_inner(batches, [0; 11], None, Vec::new(), Vec::new(), false)
7028    }
7029
7030    fn ingest_formula_batches_inner(
7031        &mut self,
7032        batches: Vec<FormulaIngestBatch>,
7033        source_counts: [u64; 11],
7034        source_report: Option<crate::engine::FormulaCompressedSourceReport>,
7035        compressed_families: Vec<(String, Vec<crate::engine::SourceFormulaFamily>)>,
7036        partitioned_families: Vec<(String, Vec<crate::engine::PartitionedSourceFormulaFamily>)>,
7037        publish_report: bool,
7038    ) -> Result<FormulaIngestReport, ExcelError> {
7039        self.observe_function_semantic_epoch()?;
7040        let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
7041        #[cfg(feature = "tracing")]
7042        let arena_nodes_before = self.graph.data_store().memory_usage().total_ast_nodes;
7043        #[cfg(feature = "tracing")]
7044        let route = if !partitioned_families.is_empty() {
7045            "partitioned"
7046        } else if !compressed_families.is_empty() {
7047            "compressed_source"
7048        } else if source_report.is_some() {
7049            "replay"
7050        } else {
7051            "ordinary"
7052        };
7053        let _ingest_span = crate::engine::trace::fz_span!(
7054            tracing::Level::INFO,
7055            "ingest",
7056            "ingest.batch",
7057            mode = ?FormulaPlaneMode::Off,
7058            route,
7059            formula_cells = formula_cells_seen
7060        );
7061        let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
7062        let materialize_batches = batches;
7063        report.formula_cells_seen = formula_cells_seen;
7064        report.source_formula_events = source_counts[0];
7065        report.source_ordinary_events = source_counts[1];
7066        report.source_shared_anchor_events = source_counts[2];
7067        report.source_shared_descendant_events = source_counts[3];
7068        report.source_unknown_events = source_counts[4];
7069        report.source_formula_records_spooled = source_counts[5];
7070        report.source_spool_encoded_bytes = source_counts[6];
7071        report.source_spool_peak_memory_bytes = source_counts[7];
7072        report.source_spool_spilled_bytes = source_counts[8];
7073        report.source_spool_spill_files = source_counts[9];
7074        report.source_spool_replays = source_counts[10];
7075        if let Some(source) = source_report {
7076            report.source_families_seen = source.families_seen;
7077            report.source_family_cells_seen = source.family_cells_seen;
7078            report.source_family_shadow_eligible = source.source_clean_families;
7079            report.source_family_shadow_eligible_cells = source.source_clean_cells;
7080            report.source_partitioned_families_seen = source.source_fragmentable_families;
7081            report.source_partition_holes = source.source_hole_exclusions;
7082            report.source_partition_ordinary_exceptions = source.source_ordinary_exclusions;
7083            report.source_partition_failures = source.source_partition_failures;
7084            report.source_partition_surviving_cells = source.source_fragmentable_cells;
7085            report.source_family_fallback = report
7086                .source_family_fallback
7087                .saturating_add(source.replay_families);
7088            report.source_family_fallback_cells = report
7089                .source_family_fallback_cells
7090                .saturating_add(source.replay_cells);
7091            report.source_forward_descendants = source.forward_descendants;
7092            report.source_evidence_limit_fallbacks = source.evidence_limit_fallbacks;
7093            report.source_evidence_peak_bytes = source.evidence_peak_bytes;
7094            for (reason, count) in source.fallback_reasons {
7095                let total = report.fallback_reasons.entry(reason).or_default();
7096                *total = total.saturating_add(count);
7097            }
7098        }
7099
7100        // A first load without graph admission plans in the builder, one
7101        // chunk at a time (a load error fails the load, so the builder's
7102        // partial application on a planning error is unobservable).
7103        if self.graph.first_load_assume_new()
7104            && !self.graph_admission_enabled()
7105            && !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
7106        {
7107            let mut builder =
7108                crate::engine::ingest_builder::BulkIngestBuilder::new(&mut self.graph);
7109            for batch in materialize_batches {
7110                if batch.is_empty() {
7111                    continue;
7112                }
7113                let sheet_id = builder
7114                    .add_sheet_checked(&batch.sheet_name)
7115                    .ok_or_else(|| {
7116                        ExcelError::new(ExcelErrorKind::Ref)
7117                            .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7118                    })?;
7119                builder.add_formula_refs(
7120                    sheet_id,
7121                    batch.formulas.into_iter().map(|record| {
7122                        (
7123                            record.row,
7124                            record.col,
7125                            crate::engine::graph::FormulaRef::of_ingested(
7126                                record.ast_id,
7127                                record.member_anchor,
7128                            ),
7129                        )
7130                    }),
7131                );
7132            }
7133            let summary = builder.finish_with_provider(&self.resolver)?;
7134            report.graph_formula_cells_materialized = summary.formulas as u64;
7135            report.graph_vertices_created = summary.vertices as u64;
7136            report.graph_edges_created = summary.edges as u64;
7137        } else if !materialize_batches.iter().all(FormulaIngestBatch::is_empty) {
7138            let mut prepared_by_sheet: BTreeMap<String, Vec<_>> = BTreeMap::new();
7139            for batch in materialize_batches {
7140                if batch.is_empty() {
7141                    continue;
7142                }
7143                let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
7144                    ExcelError::new(ExcelErrorKind::Ref)
7145                        .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7146                })?;
7147                let mut pipeline = self.ingest_pipeline();
7148                // Plan each record and keep only what the graph needs (the
7149                // pipeline's per-formula facts are dropped at once).
7150                let prepared = prepared_by_sheet.entry(batch.sheet_name).or_default();
7151                prepared.reserve(batch.formulas.len());
7152                for record in batch.formulas {
7153                    let placement = CellRef::new(
7154                        sheet_id,
7155                        Coord::from_excel(record.row, record.col, true, true),
7156                    );
7157                    let input = match record.member_anchor {
7158                        Some(anchor) => FormulaAstInput::Member {
7159                            template: record.ast_id,
7160                            anchor,
7161                        },
7162                        None => FormulaAstInput::RawArena(record.ast_id),
7163                    };
7164                    let formula = pipeline.ingest_formula(input, placement, None)?;
7165                    prepared.push((
7166                        record.row,
7167                        record.col,
7168                        crate::engine::graph::FormulaRef::of_ingested(
7169                            formula.ast_id,
7170                            formula.member_anchor,
7171                        ),
7172                        formula.dep_plan,
7173                    ));
7174                }
7175            }
7176            let admission_preflighted = self.graph_admission_enabled();
7177            if admission_preflighted {
7178                let mut preview = Vec::new();
7179                for (sheet_name, formulas) in &prepared_by_sheet {
7180                    let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(|| {
7181                        ExcelError::new(ExcelErrorKind::Ref)
7182                            .with_message(format!("unknown ingest sheet: {sheet_name}"))
7183                    })?;
7184                    preview.extend(
7185                        formulas
7186                            .iter()
7187                            .map(|(row, col, _, plan)| (sheet_id, *row, *col, plan.clone())),
7188                    );
7189                }
7190                let admission = self.graph.preview_formula_mutations(&preview)?;
7191                self.preflight_graph_admission(admission)?;
7192            }
7193
7194            let mut builder = self.begin_bulk_ingest();
7195            if admission_preflighted {
7196                builder.mark_admission_preflighted();
7197            }
7198            for (sheet_name, formulas) in prepared_by_sheet {
7199                if formulas.is_empty() {
7200                    continue;
7201                }
7202                let sheet_id = builder.add_sheet(&sheet_name);
7203                builder.add_formula_plans(sheet_id, formulas);
7204            }
7205            let summary = builder.finish()?;
7206            report.graph_formula_cells_materialized = summary.formulas as u64;
7207            report.graph_vertices_created = summary.vertices as u64;
7208            report.graph_edges_created = summary.edges as u64;
7209        }
7210
7211        crate::engine::trace::fz_event!(
7212            tracing::Level::INFO,
7213            "ingest",
7214            "ingest.summary",
7215            candidate_cells = report.shadow_candidate_cells,
7216            accepted_span_cells = report.shadow_accepted_span_cells,
7217            fallback_cells = report.shadow_fallback_cells,
7218            spans_created = report.shadow_spans_created,
7219            templates_interned = report.shadow_templates_interned,
7220            graph_vertices_created = report.graph_vertices_created,
7221            graph_edges_created = report.graph_edges_created,
7222            arena_nodes_delta = self
7223                .graph
7224                .data_store()
7225                .memory_usage()
7226                .total_ast_nodes
7227                .saturating_sub(arena_nodes_before)
7228        );
7229        if publish_report {
7230            self.record_formula_ingest_report(report.clone());
7231        }
7232        Ok(report)
7233    }
7234
7235    fn dedup_formula_parse_diagnostics_since(&mut self, start: usize) {
7236        let mut unique = Vec::new();
7237        for diagnostic in self.formula_parse_diagnostics.drain(start..) {
7238            let duplicate = unique.iter().any(|prior: &FormulaParseDiagnostic| {
7239                prior.sheet == diagnostic.sheet
7240                    && prior.row == diagnostic.row
7241                    && prior.col == diagnostic.col
7242                    && prior.formula == diagnostic.formula
7243                    && prior.policy == diagnostic.policy
7244            });
7245            if !duplicate {
7246                unique.push(diagnostic);
7247            }
7248        }
7249        self.formula_parse_diagnostics.extend(unique);
7250    }
7251
7252    pub fn handle_formula_parse_error(
7253        &mut self,
7254        sheet: &str,
7255        row: u32,
7256        col: u32,
7257        formula: &str,
7258        message: String,
7259    ) -> Result<Option<ASTNode>, ExcelError> {
7260        let policy = self.config.formula_parse_policy;
7261
7262        if policy == FormulaParsePolicy::Strict {
7263            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
7264            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
7265                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
7266            )));
7267        }
7268
7269        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
7270            sheet: sheet.to_string(),
7271            row,
7272            col,
7273            formula: formula.to_string(),
7274            message: message.clone(),
7275            policy,
7276        });
7277
7278        match policy {
7279            FormulaParsePolicy::Strict => unreachable!(),
7280            FormulaParsePolicy::KeepCachedValue => Ok(None),
7281            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
7282                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
7283                None,
7284            ))),
7285            FormulaParsePolicy::CoerceToError => {
7286                let err = ExcelError::new(ExcelErrorKind::Error)
7287                    .with_message(format!("Malformed formula: {message}"));
7288                Ok(Some(ASTNode::new(
7289                    ASTNodeType::Literal(LiteralValue::Error(err)),
7290                    None,
7291                )))
7292            }
7293        }
7294    }
7295
7296    #[cfg(test)]
7297    fn target_preparation_fault(
7298        &mut self,
7299        seam: crate::engine::target_preparation::TargetPreparationFault,
7300    ) -> Result<(), ExcelError> {
7301        if self.target_preparation_fault_for_test == Some(seam) {
7302            self.target_preparation_fault_for_test = None;
7303            Err(ExcelError::new(ExcelErrorKind::Value)
7304                .with_message(format!("injected target preparation fault: {seam:?}")))
7305        } else {
7306            Ok(())
7307        }
7308    }
7309
7310    fn preparation_stale(
7311        reason: formualizer_common::PreparationStaleReason,
7312        message: impl Into<String>,
7313    ) -> ExcelError {
7314        ExcelError::new(ExcelErrorKind::Value)
7315            .with_message(message)
7316            .with_extra(formualizer_common::ExcelErrorExtra::PreparationStale { reason })
7317    }
7318
7319    fn preparation_revision_stale_reason(
7320        assumptions: &crate::engine::PreparationRevision,
7321        current: &crate::engine::PreparationRevision,
7322        planning_requests: &BTreeSet<(String, String, usize)>,
7323        staged_leases_match: bool,
7324    ) -> Option<formualizer_common::PreparationStaleReason> {
7325        if assumptions.graph != current.graph {
7326            Some(formualizer_common::PreparationStaleReason::Graph)
7327        } else if assumptions.staged != current.staged || !staged_leases_match {
7328            Some(formualizer_common::PreparationStaleReason::Staged)
7329        } else if assumptions.symbols != current.symbols {
7330            Some(formualizer_common::PreparationStaleReason::Symbols)
7331        } else if assumptions.provider != current.provider {
7332            Some(formualizer_common::PreparationStaleReason::Provider)
7333        } else if assumptions.semantic != current.semantic
7334            && crate::function_registry::semantic_changes_affect_requests_since(
7335                assumptions.semantic,
7336                planning_requests.iter().cloned(),
7337            )
7338        {
7339            Some(formualizer_common::PreparationStaleReason::Semantic)
7340        } else {
7341            None
7342        }
7343    }
7344
7345    fn preparation_revisions(&self) -> crate::engine::PreparationRevision {
7346        crate::engine::PreparationRevision {
7347            graph: self.graph.topology_revision(),
7348            authority: 0,
7349            authority_indexes: 0,
7350            authority_indexed_plane: 0,
7351            staged: self.staged_formula_index.revision(),
7352            symbols: self.graph.symbol_revision(),
7353            semantic: crate::function_registry::semantic_epoch(),
7354            provider: self.resolver.planning_semantic_revision(),
7355        }
7356    }
7357
7358    fn planning_revision_snapshot(&self) -> PlanningRevisionSnapshot {
7359        let registry_guard = crate::function_registry::semantic_epoch_read_guard();
7360        let provider = self.resolver.planning_semantic_revision();
7361        let semantic = registry_guard.epoch();
7362        PlanningRevisionSnapshot {
7363            engine_topology_epoch: self.topology_epoch,
7364            graph_topology_revision: self.graph.topology_revision(),
7365            staged: self.staged_formula_index.revision(),
7366            symbols: self.graph.symbol_revision(),
7367            semantic,
7368            provider,
7369            deterministic_mode: self.config.deterministic_mode.clone(),
7370            budgets: self.evaluation_resource_budgets.clone(),
7371        }
7372    }
7373
7374    fn recalc_plan_key(&self) -> RecalcPlanKey {
7375        RecalcPlanKey {
7376            engine_token: Arc::clone(&self.recalc_plan_token),
7377            revisions: self.planning_revision_snapshot(),
7378        }
7379    }
7380
7381    fn plan_stale(reason: formualizer_common::PlanStaleReason) -> ExcelError {
7382        ExcelError::new(ExcelErrorKind::Value)
7383            .with_message(format!("recalculation plan is stale: {}", reason.as_str()))
7384            .with_extra(formualizer_common::ExcelErrorExtra::PlanStale { reason })
7385    }
7386
7387    fn validate_recalc_plan_key(&self, key: &RecalcPlanKey) -> Result<(), ExcelError> {
7388        use formualizer_common::PlanStaleReason;
7389
7390        if !Arc::ptr_eq(&key.engine_token, &self.recalc_plan_token) {
7391            return Err(Self::plan_stale(PlanStaleReason::Engine));
7392        }
7393
7394        let current = self.planning_revision_snapshot();
7395        let expected = &key.revisions;
7396        let stale = if expected.provider != current.provider {
7397            Some(PlanStaleReason::Provider)
7398        } else if expected.semantic != current.semantic {
7399            Some(PlanStaleReason::Semantic)
7400        } else if expected.budgets != current.budgets
7401            || expected.deterministic_mode != current.deterministic_mode
7402        {
7403            Some(PlanStaleReason::Budget)
7404        } else if expected.staged != current.staged {
7405            Some(PlanStaleReason::Staged)
7406        } else if expected.symbols != current.symbols {
7407            Some(PlanStaleReason::Symbols)
7408        } else if expected.graph_topology_revision != current.graph_topology_revision
7409            || expected.engine_topology_epoch != current.engine_topology_epoch
7410        {
7411            Some(PlanStaleReason::Graph)
7412        } else {
7413            None
7414        };
7415        stale.map_or(Ok(()), |reason| Err(Self::plan_stale(reason)))
7416    }
7417
7418    fn target_preparation_checkpoint(
7419        &mut self,
7420        deadline: Option<std::time::Instant>,
7421        work: u64,
7422    ) -> Result<(), ExcelError> {
7423        if self
7424            .active_cancel_flag
7425            .as_ref()
7426            .is_some_and(|cancel| cancel.is_cancelled())
7427        {
7428            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
7429                .with_message("target graph preparation cancelled"));
7430        }
7431        if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
7432            return Err(crate::engine::ResourceLedgerError::Exhausted(
7433                formualizer_common::ResourceExhaustionDetail {
7434                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
7435                    limit: 0,
7436                    observed: 1,
7437                    request_id: self
7438                        .active_evaluation_resource_request
7439                        .as_ref()
7440                        .map(|stats| stats.request_id),
7441                },
7442            )
7443            .into_excel_error());
7444        }
7445        self.charge_bounded_work(work)
7446    }
7447
7448    fn opaque_reason_in_ast(
7449        &self,
7450        ast: &ASTNode,
7451        provider: &dyn crate::traits::FunctionProvider,
7452    ) -> Option<crate::engine::OpaqueReason> {
7453        match &ast.node_type {
7454            ASTNodeType::Function { name, args } => {
7455                let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
7456                if canonical == "INDIRECT" {
7457                    return Some(crate::engine::OpaqueReason::RuntimeTextReference);
7458                }
7459                let Some(function) = provider.get_function_for_planning("", name) else {
7460                    return Some(crate::engine::OpaqueReason::UnknownFunction);
7461                };
7462                let caps = function.caps();
7463                if caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7464                    || caps.contains(FnCaps::RETURNS_REFERENCE)
7465                {
7466                    return Some(crate::engine::OpaqueReason::DynamicReference);
7467                }
7468                args.iter()
7469                    .find_map(|arg| self.opaque_reason_in_ast(arg, provider))
7470            }
7471            ASTNodeType::Call { .. } => Some(crate::engine::OpaqueReason::UnknownCustomFunction),
7472            ASTNodeType::UnaryOp { expr, .. } => self.opaque_reason_in_ast(expr, provider),
7473            ASTNodeType::BinaryOp { left, right, .. } => self
7474                .opaque_reason_in_ast(left, provider)
7475                .or_else(|| self.opaque_reason_in_ast(right, provider)),
7476            ASTNodeType::Array(rows) => rows
7477                .iter()
7478                .flat_map(|row| row.iter())
7479                .find_map(|item| self.opaque_reason_in_ast(item, provider)),
7480            ASTNodeType::Reference {
7481                reference:
7482                    ReferenceType::Cell {
7483                        sheet: Some(sheet), ..
7484                    }
7485                    | ReferenceType::Range {
7486                        sheet: Some(sheet), ..
7487                    },
7488                ..
7489            } if self.graph.sheet_id(sheet).is_none() => {
7490                Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding)
7491            }
7492            ASTNodeType::Reference {
7493                reference:
7494                    ReferenceType::External(_)
7495                    | ReferenceType::Cell3D { .. }
7496                    | ReferenceType::Range3D { .. },
7497                ..
7498            } => Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
7499            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => None,
7500        }
7501    }
7502
7503    fn target_planning_snapshot(
7504        &mut self,
7505        ast: &ASTNode,
7506        planning_requests: &mut BTreeSet<(String, String, usize)>,
7507    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
7508        #[cfg(test)]
7509        if std::mem::take(&mut self.inject_target_semantic_stale_once_for_test) {
7510            return Err(Self::preparation_stale(
7511                formualizer_common::PreparationStaleReason::Semantic,
7512                "injected target semantic preparation movement",
7513            ));
7514        }
7515        #[cfg(test)]
7516        if let Some(hook) = self.before_target_planning_snapshot_hook.take() {
7517            hook();
7518        }
7519        let mut requests = Vec::new();
7520        Self::collect_planning_function_requests(ast, &mut requests);
7521        requests.sort();
7522        requests.dedup();
7523        planning_requests.extend(requests.iter().cloned());
7524        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
7525            &self.resolver,
7526            requests,
7527        )
7528        .map_err(|error| {
7529            ExcelError::new(ExcelErrorKind::Value)
7530                .with_message(format!("target planning snapshot unavailable: {error:?}"))
7531        })
7532    }
7533
7534    fn target_planning_snapshot_stale_reason(
7535        snapshot: &crate::function_registry::RegistryPlanningSnapshot,
7536        assumptions: &crate::engine::PreparationRevision,
7537    ) -> Option<formualizer_common::PreparationStaleReason> {
7538        if snapshot
7539            .provider_revision()
7540            .is_some_and(|revision| Some(revision) != assumptions.provider)
7541        {
7542            Some(formualizer_common::PreparationStaleReason::Provider)
7543        } else if snapshot.epoch() != assumptions.semantic
7544            && snapshot.semantic_changes_affect_requests_since(assumptions.semantic)
7545        {
7546            Some(formualizer_common::PreparationStaleReason::Semantic)
7547        } else {
7548            None
7549        }
7550    }
7551
7552    fn widen_target_preparation(
7553        policy: crate::engine::OpaquePreparePolicy,
7554        scope: &mut crate::engine::PrepareScope,
7555        reasons: &mut Vec<crate::engine::OpaqueReason>,
7556        reason: crate::engine::OpaqueReason,
7557    ) -> Result<bool, ExcelError> {
7558        if policy == crate::engine::OpaquePreparePolicy::Error {
7559            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7560                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7561        }
7562        if !reasons.contains(&reason) {
7563            reasons.push(reason);
7564        }
7565        if !matches!(scope, crate::engine::PrepareScope::Workbook) {
7566            *scope = crate::engine::PrepareScope::Workbook;
7567            Ok(true)
7568        } else {
7569            Ok(false)
7570        }
7571    }
7572
7573    fn widen_target_preparation_to_sheet(
7574        policy: crate::engine::OpaquePreparePolicy,
7575        scope: &mut crate::engine::PrepareScope,
7576        reasons: &mut Vec<crate::engine::OpaqueReason>,
7577        reason: crate::engine::OpaqueReason,
7578        sheet: &str,
7579    ) -> Result<bool, ExcelError> {
7580        if policy == crate::engine::OpaquePreparePolicy::Error {
7581            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7582                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7583        }
7584        if !reasons.contains(&reason) {
7585            reasons.push(reason);
7586        }
7587        match scope {
7588            crate::engine::PrepareScope::Exact => {
7589                *scope = crate::engine::PrepareScope::Sheets(vec![sheet.to_string()]);
7590                Ok(true)
7591            }
7592            crate::engine::PrepareScope::Sheets(sheets) => {
7593                if sheets.iter().any(|candidate| candidate == sheet) {
7594                    Ok(false)
7595                } else {
7596                    sheets.push(sheet.to_string());
7597                    sheets.sort();
7598                    Ok(true)
7599                }
7600            }
7601            crate::engine::PrepareScope::Workbook => Ok(false),
7602        }
7603    }
7604
7605    fn ast_has_proven_sheet_local_dynamic(
7606        ast: &ASTNode,
7607        provider: &dyn crate::traits::FunctionProvider,
7608    ) -> bool {
7609        fn classify(ast: &ASTNode, provider: &dyn crate::traits::FunctionProvider) -> (bool, bool) {
7610            match &ast.node_type {
7611                ASTNodeType::Function { name, args } => {
7612                    let Some(function) = provider.get_function_for_planning("", name) else {
7613                        return (false, false);
7614                    };
7615                    let caps = function.caps();
7616                    let dynamic = caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7617                        || caps.contains(FnCaps::RETURNS_REFERENCE);
7618                    let canonical = name.rsplit('.').next().unwrap_or(name);
7619                    if dynamic
7620                        && !canonical.eq_ignore_ascii_case("OFFSET")
7621                        && !canonical.eq_ignore_ascii_case("INDEX")
7622                    {
7623                        return (false, true);
7624                    }
7625                    let mut has_dynamic = dynamic;
7626                    for arg in args {
7627                        let (safe, child_dynamic) = classify(arg, provider);
7628                        if !safe {
7629                            return (false, has_dynamic || child_dynamic);
7630                        }
7631                        has_dynamic |= child_dynamic;
7632                    }
7633                    (true, has_dynamic)
7634                }
7635                ASTNodeType::UnaryOp { expr, .. } => classify(expr, provider),
7636                ASTNodeType::BinaryOp { left, right, .. } => {
7637                    let (left_safe, left_dynamic) = classify(left, provider);
7638                    let (right_safe, right_dynamic) = classify(right, provider);
7639                    (left_safe && right_safe, left_dynamic || right_dynamic)
7640                }
7641                ASTNodeType::Array(rows) => {
7642                    let mut has_dynamic = false;
7643                    for item in rows.iter().flatten() {
7644                        let (safe, child_dynamic) = classify(item, provider);
7645                        if !safe {
7646                            return (false, has_dynamic || child_dynamic);
7647                        }
7648                        has_dynamic |= child_dynamic;
7649                    }
7650                    (true, has_dynamic)
7651                }
7652                ASTNodeType::Reference {
7653                    reference:
7654                        ReferenceType::Cell { sheet: None, .. }
7655                        | ReferenceType::Range { sheet: None, .. },
7656                    ..
7657                }
7658                | ASTNodeType::Literal(_)
7659                | ASTNodeType::Omitted => (true, false),
7660                ASTNodeType::Call { .. } | ASTNodeType::Reference { .. } => (false, false),
7661            }
7662        }
7663
7664        let (safe, dynamic) = classify(ast, provider);
7665        safe && dynamic
7666    }
7667
7668    fn table_selection_region(
7669        &self,
7670        entry: &crate::engine::graph::TableEntry,
7671        selection: &crate::engine::TableSelection,
7672    ) -> Result<PreparationRegion, ExcelError> {
7673        let mut start_row = entry.range.start.coord.row() + 1;
7674        let mut end_row = entry.range.end.coord.row() + 1;
7675        let mut start_col = entry.range.start.coord.col() + 1;
7676        let mut end_col = entry.range.end.coord.col() + 1;
7677        match selection {
7678            crate::engine::TableSelection::Whole => {}
7679            crate::engine::TableSelection::Headers => {
7680                if !entry.header_row {
7681                    return Err(ExcelError::new(ExcelErrorKind::Value)
7682                        .with_message(format!("table {} has no header row", entry.name)));
7683                }
7684                end_row = start_row;
7685            }
7686            crate::engine::TableSelection::Data => {
7687                if entry.header_row {
7688                    start_row = start_row.saturating_add(1);
7689                }
7690                if entry.totals_row {
7691                    end_row = end_row.saturating_sub(1);
7692                }
7693            }
7694            crate::engine::TableSelection::Totals => {
7695                if !entry.totals_row {
7696                    return Err(ExcelError::new(ExcelErrorKind::Value)
7697                        .with_message(format!("table {} has no totals row", entry.name)));
7698                }
7699                start_row = end_row;
7700            }
7701            crate::engine::TableSelection::Column(column) => {
7702                let index = entry.col_index(column).ok_or_else(|| {
7703                    ExcelError::new(ExcelErrorKind::Name)
7704                        .with_message(format!("unknown table column: {column}"))
7705                })?;
7706                start_col = start_col.saturating_add(index as u32);
7707                end_col = start_col;
7708            }
7709            crate::engine::TableSelection::Columns { start, end } => {
7710                let first = entry.col_index(start).ok_or_else(|| {
7711                    ExcelError::new(ExcelErrorKind::Name)
7712                        .with_message(format!("unknown table column: {start}"))
7713                })?;
7714                let last = entry.col_index(end).ok_or_else(|| {
7715                    ExcelError::new(ExcelErrorKind::Name)
7716                        .with_message(format!("unknown table column: {end}"))
7717                })?;
7718                if first > last {
7719                    return Err(ExcelError::new(ExcelErrorKind::Value)
7720                        .with_message("table column selection is reversed"));
7721                }
7722                start_col = start_col.saturating_add(first as u32);
7723                end_col = entry
7724                    .range
7725                    .start
7726                    .coord
7727                    .col()
7728                    .saturating_add(last as u32)
7729                    .saturating_add(1);
7730            }
7731        }
7732        if start_row > end_row && matches!(selection, crate::engine::TableSelection::Data) {
7733            start_row = entry.range.start.coord.row() + 1;
7734            end_row = start_row;
7735        }
7736        if start_row > end_row || start_col > end_col {
7737            return Err(
7738                ExcelError::new(ExcelErrorKind::Value).with_message("table selection is empty")
7739            );
7740        }
7741        Ok(PreparationRegion {
7742            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
7743            sheet_id: entry.sheet_id(),
7744            start_row,
7745            start_col,
7746            end_row,
7747            end_col,
7748        })
7749    }
7750
7751    #[cfg(test)]
7752    pub(crate) fn reset_target_root_dedup_probes_for_test() {
7753        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(0));
7754    }
7755
7756    #[cfg(test)]
7757    pub(crate) fn target_root_dedup_probes_for_test() -> usize {
7758        TARGET_ROOT_DEDUP_PROBES.with(std::cell::Cell::get)
7759    }
7760
7761    pub(crate) fn resolve_target_producers(
7762        &mut self,
7763        targets: &[crate::engine::EvaluationTarget],
7764    ) -> Result<Vec<crate::engine::target_preparation::TargetProducer>, ExcelError> {
7765        use crate::engine::target_preparation::TargetProducer;
7766
7767        let request_id = self
7768            .active_evaluation_resource_request
7769            .as_ref()
7770            .map(|request| request.request_id);
7771        let mut roots = OrderedTargetProducers::with_capacity(targets.len())
7772            .map_err(|_| target_root_allocation_error(targets.len(), request_id))?;
7773        let resolve_region = |engine: &mut Self,
7774                              region: Region,
7775                              value_only: Option<CellRef>,
7776                              roots: &mut OrderedTargetProducers|
7777         -> Result<(), ExcelError> {
7778            let before = roots.len();
7779            for anchor in engine.graph.spill_anchors_in_region(
7780                region.sheet_id(),
7781                region.axis_ranges().0.query_bounds().0,
7782                region.axis_ranges().1.query_bounds().0,
7783                region.axis_ranges().0.query_bounds().1,
7784                region.axis_ranges().1.query_bounds().1,
7785            ) {
7786                roots
7787                    .push(TargetProducer::Legacy(anchor))
7788                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7789            }
7790            for vertex in engine.graph.vertices_in_region(
7791                region.sheet_id(),
7792                region.axis_ranges().0.query_bounds().0,
7793                region.axis_ranges().0.query_bounds().1,
7794                region.axis_ranges().1.query_bounds().0,
7795                region.axis_ranges().1.query_bounds().1,
7796            ) {
7797                let vertex = engine
7798                    .graph
7799                    .get_cell_ref(vertex)
7800                    .and_then(|cell| engine.graph.spill_registry_anchor_for_cell(cell))
7801                    .unwrap_or(vertex);
7802                // `vertices_in_region` is a sheet-index query and a sheet index holds only
7803                // grid-addressed vertices, so a region can never yield a symbol: names,
7804                // tables and external sources have no position for a region to cover.
7805                // Symbol roots come from the by-name lookups below instead.
7806                if matches!(
7807                    engine.graph.get_vertex_kind(vertex),
7808                    VertexKind::FormulaScalar | VertexKind::FormulaArray
7809                ) {
7810                    roots
7811                        .push(TargetProducer::Legacy(vertex))
7812                        .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7813                }
7814            }
7815            if roots.len() == before
7816                && let Some(cell) = value_only
7817            {
7818                roots
7819                    .push(TargetProducer::ValueOnly(cell))
7820                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7821            }
7822            Ok(())
7823        };
7824
7825        for target in targets {
7826            match target {
7827                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
7828                    if *row == 0 || *col == 0 {
7829                        return Err(ExcelError::new(ExcelErrorKind::Ref)
7830                            .with_message("target cell coordinates are one-based"));
7831                    }
7832                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
7833                        ExcelError::new(ExcelErrorKind::Ref)
7834                            .with_message(format!("target sheet not found: {sheet}"))
7835                    })?;
7836                    let cell = CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true));
7837                    resolve_region(
7838                        self,
7839                        Region::point(sheet_id, *row - 1, *col - 1),
7840                        Some(cell),
7841                        &mut roots,
7842                    )?;
7843                }
7844                crate::engine::EvaluationTarget::Range(range) => {
7845                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
7846                        ExcelError::new(ExcelErrorKind::Ref)
7847                            .with_message(format!("target sheet not found: {}", range.sheet))
7848                    })?;
7849                    resolve_region(
7850                        self,
7851                        Region::rect(
7852                            sheet_id,
7853                            range.start_row - 1,
7854                            range.end_row - 1,
7855                            range.start_col - 1,
7856                            range.end_col - 1,
7857                        ),
7858                        None,
7859                        &mut roots,
7860                    )?;
7861                }
7862                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
7863                    let scope = self.name_query_scope(scope_sheet.as_deref())?;
7864                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, scope) {
7865                        roots
7866                            .push(TargetProducer::Symbol(entry.vertex))
7867                            .map_err(|_| {
7868                                target_root_allocation_error(roots.len() + 1, request_id)
7869                            })?;
7870                    }
7871                }
7872                crate::engine::EvaluationTarget::Table { name, .. } => {
7873                    if let Some(entry) = self.graph.resolve_table_entry(name) {
7874                        roots
7875                            .push(TargetProducer::Symbol(entry.vertex))
7876                            .map_err(|_| {
7877                                target_root_allocation_error(roots.len() + 1, request_id)
7878                            })?;
7879                    }
7880                }
7881            }
7882        }
7883        Ok(roots.into_vec())
7884    }
7885
7886    /// Transactionally prepare the complete ordinary staged demand closure for typed targets.
7887    ///
7888    /// This method prepares graph topology only. It does not evaluate target values.
7889    pub fn prepare_graph_for_targets(
7890        &mut self,
7891        targets: &[crate::engine::EvaluationTarget],
7892        options: crate::engine::TargetEvalOptions<'_>,
7893    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7894        let previous_budgets = self.evaluation_resource_budgets.clone();
7895        let diagnostics_len = self.formula_parse_diagnostics.len();
7896        let previous_report = self.last_formula_ingest_report.clone();
7897        if let Some(budgets) = options.budgets {
7898            self.evaluation_resource_budgets = budgets.clone();
7899        }
7900        let previous_graph_budget_override = self
7901            .graph
7902            .set_admission_budget_override(Some(self.evaluation_resource_budgets.clone()));
7903        // Hoist the call's cancellation onto the engine for its duration, so the
7904        // preparation checkpoints observe it. This is a standalone entry point, so
7905        // the previous value is restored rather than cleared.
7906        let previous_cancel = self.active_cancel_flag.take();
7907        self.active_cancel_flag = options.cancel.clone();
7908        let result = self.observe_evaluation_resource_request(
7909            EvaluationRequestKind::TargetPreparation,
7910            |engine| engine.prepare_graph_for_targets_unobserved(targets, &options),
7911        );
7912        self.active_cancel_flag = previous_cancel;
7913        self.graph
7914            .set_admission_budget_override(previous_graph_budget_override);
7915        self.evaluation_resource_budgets = previous_budgets;
7916        if result.is_err() {
7917            self.formula_parse_diagnostics.truncate(diagnostics_len);
7918            self.last_formula_ingest_report = previous_report;
7919        }
7920        result
7921    }
7922
7923    fn prepare_graph_for_targets_unobserved(
7924        &mut self,
7925        targets: &[crate::engine::EvaluationTarget],
7926        options: &crate::engine::TargetEvalOptions<'_>,
7927    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7928        let scratch_checkpoint = self
7929            .active_resource_ledger
7930            .as_ref()
7931            .map_or(0, crate::engine::ResourceLedger::scratch_checkpoint);
7932        let result = self.prepare_graph_for_targets_transaction(targets, options);
7933        let release = self
7934            .active_resource_ledger
7935            .as_mut()
7936            .map_or(Ok(()), |ledger| {
7937                ledger.release_scratch_to(scratch_checkpoint)
7938            });
7939        match (result, release) {
7940            (result, Ok(())) => result,
7941            (Ok(_), Err(error)) | (Err(_), Err(error)) => Err(error.into_excel_error()),
7942        }
7943    }
7944
7945    fn prepare_graph_for_targets_transaction(
7946        &mut self,
7947        targets: &[crate::engine::EvaluationTarget],
7948        options: &crate::engine::TargetEvalOptions<'_>,
7949    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7950        use crate::engine::{
7951            OpaqueReason, PreparationOutcome, PrepareScope, PreparedTargetGraphReport,
7952            TableSelection,
7953        };
7954
7955        self.target_preparation_checkpoint(options.deadline, 0)?;
7956        self.observe_function_semantic_epoch()?;
7957        let assumptions = self.preparation_revisions();
7958        let ledger_at_start = self
7959            .active_resource_ledger
7960            .as_ref()
7961            .map(|ledger| ledger.snapshot());
7962        let diagnostics_len = self.formula_parse_diagnostics.len();
7963        let report_len = self.last_formula_ingest_report.clone();
7964        let request_id = options.request_id.or_else(|| {
7965            self.active_evaluation_resource_request
7966                .as_ref()
7967                .map(|stats| stats.request_id)
7968        });
7969
7970        let mut scope = PrepareScope::Exact;
7971        let mut reasons = Vec::new();
7972        let mut regions = VecDeque::new();
7973        let mut deferred_shared_regions = VecDeque::new();
7974        let mut normalized = Vec::with_capacity(targets.len());
7975        let mut symbol_vertices = VecDeque::new();
7976        for target in targets {
7977            self.target_preparation_checkpoint(options.deadline, 1)?;
7978            match target {
7979                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
7980                    if *row == 0 || *col == 0 {
7981                        return Err(ExcelError::new(ExcelErrorKind::Ref)
7982                            .with_message("target cell coordinates are one-based"));
7983                    }
7984                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
7985                        ExcelError::new(ExcelErrorKind::Ref)
7986                            .with_message(format!("target sheet not found: {sheet}"))
7987                    })?;
7988                    regions.push_back(PreparationRegion {
7989                        sheet: sheet.clone(),
7990                        sheet_id,
7991                        start_row: *row,
7992                        start_col: *col,
7993                        end_row: *row,
7994                        end_col: *col,
7995                    });
7996                    normalized.push(target.clone());
7997                }
7998                crate::engine::EvaluationTarget::Range(range) => {
7999                    if range.start_row == 0
8000                        || range.start_col == 0
8001                        || range.end_row < range.start_row
8002                        || range.end_col < range.start_col
8003                    {
8004                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8005                            .with_message("invalid target range"));
8006                    }
8007                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8008                        ExcelError::new(ExcelErrorKind::Ref)
8009                            .with_message(format!("target sheet not found: {}", range.sheet))
8010                    })?;
8011                    regions.push_back(PreparationRegion {
8012                        sheet: range.sheet.clone(),
8013                        sheet_id,
8014                        start_row: range.start_row,
8015                        start_col: range.start_col,
8016                        end_row: range.end_row,
8017                        end_col: range.end_col,
8018                    });
8019                    normalized.push(target.clone());
8020                }
8021                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
8022                    let name_scope = self.name_query_scope(scope_sheet.as_deref())?;
8023                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, name_scope) {
8024                        symbol_vertices.push_back(entry.vertex);
8025                    } else {
8026                        Self::widen_target_preparation(
8027                            options.opaque_policy,
8028                            &mut scope,
8029                            &mut reasons,
8030                            OpaqueReason::UnresolvedName,
8031                        )?;
8032                    }
8033                    normalized.push(target.clone());
8034                }
8035                crate::engine::EvaluationTarget::Table { name, selection } => {
8036                    if let Some(entry) = self.graph.resolve_table_entry(name) {
8037                        let region = self.table_selection_region(entry, selection)?;
8038                        symbol_vertices.push_back(entry.vertex);
8039                        regions.push_back(region);
8040                    } else {
8041                        Self::widen_target_preparation(
8042                            options.opaque_policy,
8043                            &mut scope,
8044                            &mut reasons,
8045                            OpaqueReason::UnresolvedTable,
8046                        )?;
8047                    }
8048                    normalized.push(target.clone());
8049                }
8050            }
8051        }
8052
8053        let mut visited_regions = FxHashSet::default();
8054        let mut visited_vertices = FxHashSet::default();
8055        let mut selected = FxHashSet::default();
8056        let mut prepared = Vec::new();
8057        let mut pending_diagnostics = Vec::new();
8058        let mut planning_requests = BTreeSet::new();
8059        let mut selected_cells = Vec::new();
8060        let mut workbook_seeded = false;
8061        let mut sheet_scope_seeded = BTreeSet::new();
8062        let mut indexed_query_sheets = FxHashSet::default();
8063        let mut discovery_scratch_reserved = 0u64;
8064        let mut package_encountered = false;
8065        let mut selected_package_sheets = FxHashSet::default();
8066        let mut selected_package_points: BTreeMap<String, BTreeSet<(u32, u32)>> = BTreeMap::new();
8067        let mut prepared_packages: Vec<PreparedTargetSourcePackage> = Vec::new();
8068        let authoritative_with_ordinary = false;
8069        let has_unknown_package_sheet = self
8070            .staged_formula_index
8071            .package_sheets()
8072            .any(|sheet| self.graph.sheet_id(sheet).is_none());
8073
8074        loop {
8075            if let PrepareScope::Sheets(sheets) = &scope {
8076                for sheet in sheets.clone() {
8077                    if !sheet_scope_seeded.insert(sheet.clone()) {
8078                        continue;
8079                    }
8080                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8081                        package_encountered = true;
8082                        Self::widen_target_preparation(
8083                            options.opaque_policy,
8084                            &mut scope,
8085                            &mut reasons,
8086                            OpaqueReason::UnsupportedSourceSemantics,
8087                        )?;
8088                        continue;
8089                    };
8090                    for lease in self.staged_formula_index.leases_for_sheet(&sheet) {
8091                        self.target_preparation_checkpoint(options.deadline, 1)?;
8092                        regions.push_back(PreparationRegion {
8093                            sheet: sheet.clone(),
8094                            sheet_id,
8095                            start_row: lease.row,
8096                            start_col: lease.col,
8097                            end_row: lease.row,
8098                            end_col: lease.col,
8099                        });
8100                    }
8101                    if self
8102                        .staged_formula_index
8103                        .package_lease_for_sheet(&sheet)
8104                        .is_some()
8105                    {
8106                        regions.push_back(PreparationRegion {
8107                            sheet: sheet.clone(),
8108                            sheet_id,
8109                            start_row: 1,
8110                            start_col: 1,
8111                            end_row: self.workbook_load_limits.max_sheet_rows,
8112                            end_col: self.workbook_load_limits.max_sheet_cols,
8113                        });
8114                    }
8115                }
8116            }
8117            if matches!(scope, PrepareScope::Workbook) && !workbook_seeded {
8118                workbook_seeded = true;
8119                for (sheet, lease) in self.staged_formula_index.all_leases() {
8120                    self.target_preparation_checkpoint(options.deadline, 1)?;
8121                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8122                        package_encountered = true;
8123                        Self::widen_target_preparation(
8124                            options.opaque_policy,
8125                            &mut scope,
8126                            &mut reasons,
8127                            OpaqueReason::UnsupportedSourceSemantics,
8128                        )?;
8129                        continue;
8130                    };
8131                    regions.push_back(PreparationRegion {
8132                        sheet,
8133                        sheet_id,
8134                        start_row: lease.row,
8135                        start_col: lease.col,
8136                        end_row: lease.row,
8137                        end_col: lease.col,
8138                    });
8139                }
8140                let package_sheets = self
8141                    .staged_formula_index
8142                    .package_sheets()
8143                    .map(str::to_string)
8144                    .collect::<Vec<_>>();
8145                for sheet in package_sheets {
8146                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8147                        package_encountered = true;
8148                        Self::widen_target_preparation(
8149                            options.opaque_policy,
8150                            &mut scope,
8151                            &mut reasons,
8152                            OpaqueReason::UnsupportedSourceSemantics,
8153                        )?;
8154                        continue;
8155                    };
8156                    regions.push_back(PreparationRegion {
8157                        sheet,
8158                        sheet_id,
8159                        start_row: 1,
8160                        start_col: 1,
8161                        end_row: self.workbook_load_limits.max_sheet_rows,
8162                        end_col: self.workbook_load_limits.max_sheet_cols,
8163                    });
8164                }
8165            }
8166
8167            // Finish known ordinary/name dependency discovery before expanding
8168            // partial shared demands. A queued SUM may complete those families.
8169            let allow_partial_shared = regions.is_empty() && symbol_vertices.is_empty();
8170            let next_region = if allow_partial_shared {
8171                deferred_shared_regions.pop_front()
8172            } else {
8173                regions.pop_front()
8174            };
8175            let Some(region) = next_region else {
8176                if let Some(vertex) = symbol_vertices.pop_front() {
8177                    self.target_preparation_checkpoint(options.deadline, 1)?;
8178                    if !visited_vertices.insert(vertex) || !self.graph.vertex_exists(vertex) {
8179                        continue;
8180                    }
8181                    let vertex_is_dynamic = self.graph.is_dynamic(vertex);
8182                    if let Some(ast) = self.graph.get_formula(vertex) {
8183                        let snapshot =
8184                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8185                        if let Some(reason) =
8186                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8187                        {
8188                            return Err(Self::preparation_stale(
8189                                reason,
8190                                "target planning snapshot became stale during discovery",
8191                            ));
8192                        }
8193                        let opaque = self.opaque_reason_in_ast(&ast, &snapshot);
8194                        if let Some(reason) =
8195                            opaque.or(vertex_is_dynamic.then_some(OpaqueReason::DynamicReference))
8196                        {
8197                            if reason == OpaqueReason::DynamicReference
8198                                && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
8199                            {
8200                                let sheet = self.graph.get_vertex_sheet_id(vertex);
8201                                let sheet = self.graph.sheet_name(sheet).to_string();
8202                                Self::widen_target_preparation_to_sheet(
8203                                    options.opaque_policy,
8204                                    &mut scope,
8205                                    &mut reasons,
8206                                    reason,
8207                                    &sheet,
8208                                )?;
8209                            } else {
8210                                Self::widen_target_preparation(
8211                                    options.opaque_policy,
8212                                    &mut scope,
8213                                    &mut reasons,
8214                                    reason,
8215                                )?;
8216                            }
8217                        }
8218                    } else if vertex_is_dynamic {
8219                        Self::widen_target_preparation(
8220                            options.opaque_policy,
8221                            &mut scope,
8222                            &mut reasons,
8223                            OpaqueReason::DynamicReference,
8224                        )?;
8225                    }
8226                    if let Some(anchor) = self
8227                        .graph
8228                        .get_cell_ref(vertex)
8229                        .and_then(|cell| self.graph.spill_registry_anchor_for_cell(cell))
8230                    {
8231                        symbol_vertices.push_back(anchor);
8232                    }
8233                    if let Some(cell) = self.graph.get_cell_ref(vertex) {
8234                        let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
8235                        regions.push_back(PreparationRegion {
8236                            sheet,
8237                            sheet_id: cell.sheet_id,
8238                            start_row: cell.coord.row() + 1,
8239                            start_col: cell.coord.col() + 1,
8240                            end_row: cell.coord.row() + 1,
8241                            end_col: cell.coord.col() + 1,
8242                        });
8243                    }
8244                    // The formula's direct precedents, from the authority:
8245                    // cells and ranges become regions, symbol rows (names,
8246                    // tables, sources) their vertices.
8247                    match self.graph.authority_vertex_precedents(vertex) {
8248                        Some(precedents) => {
8249                            for (sheet_id, rect) in precedents {
8250                                self.target_preparation_checkpoint(options.deadline, 1)?;
8251                                if sheet_id == crate::engine::authority::geom::SYMBOL_SHEET {
8252                                    for slot in rect.r0..=rect.r1 {
8253                                        if let Some(symbol) =
8254                                            self.graph.authority_host().symbols().vertex(slot)
8255                                        {
8256                                            symbol_vertices.push_back(symbol);
8257                                        }
8258                                    }
8259                                    continue;
8260                                }
8261                                let sheet = self.graph.sheet_name(sheet_id).to_string();
8262                                regions.push_back(PreparationRegion {
8263                                    sheet,
8264                                    sheet_id,
8265                                    start_row: rect.r0 + 1,
8266                                    start_col: rect.c0 + 1,
8267                                    end_row: (rect.r1 + 1)
8268                                        .min(self.workbook_load_limits.max_sheet_rows),
8269                                    end_col: (rect.c1 + 1)
8270                                        .min(self.workbook_load_limits.max_sheet_cols),
8271                                });
8272                            }
8273                        }
8274                        // The authority cannot answer (failed host): widen.
8275                        None => {
8276                            if Self::widen_target_preparation(
8277                                options.opaque_policy,
8278                                &mut scope,
8279                                &mut reasons,
8280                                OpaqueReason::UnresolvedCrossSheetBinding,
8281                            )? {
8282                                continue;
8283                            }
8284                        }
8285                    }
8286                    if let Some(name) = self.graph.named_range_by_vertex(vertex).cloned() {
8287                        match &name.definition {
8288                            NamedDefinition::Cell(cell) => regions.push_back(PreparationRegion {
8289                                sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
8290                                sheet_id: cell.sheet_id,
8291                                start_row: cell.coord.row() + 1,
8292                                start_col: cell.coord.col() + 1,
8293                                end_row: cell.coord.row() + 1,
8294                                end_col: cell.coord.col() + 1,
8295                            }),
8296                            NamedDefinition::Range(range) => regions.push_back(PreparationRegion {
8297                                sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
8298                                sheet_id: range.start.sheet_id,
8299                                start_row: range.start.coord.row() + 1,
8300                                start_col: range.start.coord.col() + 1,
8301                                end_row: range.end.coord.row() + 1,
8302                                end_col: range.end.coord.col() + 1,
8303                            }),
8304                            NamedDefinition::Formula {
8305                                ast,
8306                                dependencies,
8307                                range_deps,
8308                            } => {
8309                                let snapshot =
8310                                    self.target_planning_snapshot(ast, &mut planning_requests)?;
8311                                if let Some(reason) = Self::target_planning_snapshot_stale_reason(
8312                                    &snapshot,
8313                                    &assumptions,
8314                                ) {
8315                                    return Err(Self::preparation_stale(
8316                                        reason,
8317                                        "target planning snapshot became stale during discovery",
8318                                    ));
8319                                }
8320                                if let Some(reason) = self.opaque_reason_in_ast(ast, &snapshot) {
8321                                    Self::widen_target_preparation(
8322                                        options.opaque_policy,
8323                                        &mut scope,
8324                                        &mut reasons,
8325                                        reason,
8326                                    )?;
8327                                }
8328                                for dependency in dependencies {
8329                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8330                                    symbol_vertices.push_back(*dependency);
8331                                }
8332                                for range in range_deps {
8333                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8334                                    // `Current` is the sheet this name's formula
8335                                    // was interpreted on, which is the sheet its
8336                                    // vertex is placed on -- the same derivation
8337                                    // the formula-vertex arm above uses. It is
8338                                    // never the workbook default sheet, and an
8339                                    // unresolvable `Name` widens instead of
8340                                    // silently landing on some other sheet.
8341                                    let context_sheet = self.graph.get_vertex_sheet_id(vertex);
8342                                    let Ok(sheet_id) =
8343                                        self.resolve_sheet_locator(&range.sheet, context_sheet)
8344                                    else {
8345                                        if Self::widen_target_preparation(
8346                                            options.opaque_policy,
8347                                            &mut scope,
8348                                            &mut reasons,
8349                                            OpaqueReason::UnresolvedCrossSheetBinding,
8350                                        )? {
8351                                            break;
8352                                        }
8353                                        continue;
8354                                    };
8355                                    regions.push_back(PreparationRegion {
8356                                        sheet: self.graph.sheet_name(sheet_id).to_string(),
8357                                        sheet_id,
8358                                        start_row: range
8359                                            .start_row
8360                                            .map_or(1, |bound| bound.index + 1),
8361                                        start_col: range
8362                                            .start_col
8363                                            .map_or(1, |bound| bound.index + 1),
8364                                        end_row: range.end_row.map_or(
8365                                            self.workbook_load_limits.max_sheet_rows,
8366                                            |bound| bound.index + 1,
8367                                        ),
8368                                        end_col: range.end_col.map_or(
8369                                            self.workbook_load_limits.max_sheet_cols,
8370                                            |bound| bound.index + 1,
8371                                        ),
8372                                    });
8373                                }
8374                            }
8375                            NamedDefinition::Literal(_) => {}
8376                        }
8377                    }
8378                    if let Some(table) = self.graph.table_by_vertex(vertex) {
8379                        regions
8380                            .push_back(self.table_selection_region(table, &TableSelection::Whole)?);
8381                    }
8382                    continue;
8383                }
8384                break;
8385            };
8386
8387            self.target_preparation_checkpoint(options.deadline, 1)?;
8388            if !visited_regions.insert(region.clone()) && !allow_partial_shared {
8389                continue;
8390            }
8391            if let PrepareScope::Sheets(sheets) = &mut scope
8392                && !sheets.iter().any(|sheet| sheet == &region.sheet)
8393            {
8394                sheets.push(region.sheet.clone());
8395                sheets.sort();
8396            }
8397            let package_match = self.staged_formula_index.package_for_region(
8398                &region.sheet,
8399                region.start_row,
8400                region.start_col,
8401                region.end_row,
8402                region.end_col,
8403            );
8404            let package_lease = match package_match {
8405                Some(Ok(lease)) => Some(lease),
8406                Some(Err(()))
8407                    if region.start_row == 1
8408                        && region.start_col == 1
8409                        && region.end_row == self.workbook_load_limits.max_sheet_rows
8410                        && region.end_col == self.workbook_load_limits.max_sheet_cols =>
8411                {
8412                    self.staged_formula_index
8413                        .package_lease_for_sheet(&region.sheet)
8414                }
8415                Some(Err(())) => {
8416                    Self::widen_target_preparation(
8417                        options.opaque_policy,
8418                        &mut scope,
8419                        &mut reasons,
8420                        OpaqueReason::DeferredSourcePackage,
8421                    )?;
8422                    None
8423                }
8424                None => None,
8425            };
8426            let compatibility_before_package_replay = package_lease.is_some()
8427                && (authoritative_with_ordinary || has_unknown_package_sheet);
8428            let package_lease = if compatibility_before_package_replay {
8429                package_encountered = true;
8430                Self::widen_target_preparation(
8431                    options.opaque_policy,
8432                    &mut scope,
8433                    &mut reasons,
8434                    OpaqueReason::UnsupportedSourceSemantics,
8435                )?;
8436                None
8437            } else {
8438                package_lease
8439            };
8440            if let Some(package_lease) = package_lease
8441                && !selected_package_sheets.contains(&region.sheet)
8442            {
8443                self.target_preparation_checkpoint(options.deadline, 1)?;
8444                let mut points = self.staged_formula_index.package_points_in_region(
8445                    &region.sheet,
8446                    region.start_row,
8447                    region.start_col,
8448                    region.end_row,
8449                    region.end_col,
8450                );
8451                if let Some(selected) = selected_package_points.get(&region.sheet) {
8452                    points.retain(|point| !selected.contains(point));
8453                }
8454
8455                if let Some(selected) = selected_package_points.get(&region.sheet) {
8456                    points.retain(|point| !selected.contains(point));
8457                }
8458                let permit_partial = allow_partial_shared
8459                    || (points.len() == 1
8460                        && regions.is_empty()
8461                        && symbol_vertices.is_empty()
8462                        && deferred_shared_regions.is_empty());
8463                let partial = self.prepare_target_exact_source_selection(
8464                    &region.sheet,
8465                    package_lease,
8466                    points,
8467                    selected_package_points
8468                        .get(&region.sheet)
8469                        .unwrap_or(&BTreeSet::new()),
8470                    permit_partial,
8471                    options.deadline,
8472                    &mut discovery_scratch_reserved,
8473                )?;
8474                let mut package = if let Some(mut package) = partial {
8475                    if package.deferred_shared {
8476                        deferred_shared_regions.push_back(region.clone());
8477                    }
8478                    // A later range can complete a family touched earlier in this
8479                    // request. Retire only that family's earlier legacy proposals;
8480                    // nothing has been published and its dependencies remain demanded.
8481                    if !package.direct_domains.is_empty() {
8482                        for prior in prepared_packages
8483                            .iter_mut()
8484                            .filter(|prior| prior.sheet == region.sheet)
8485                        {
8486                            prior
8487                                .replay_records
8488                                .retain(|record| !package.direct_contains(record.row, record.col));
8489                            prior
8490                                .legacy
8491                                .retain(|(row, col, _, _)| !package.direct_contains(*row, *col));
8492                            if let Some(points) = prior.selected_points.as_mut() {
8493                                points.retain(|&(row, col)| !package.direct_contains(row, col));
8494                            }
8495                        }
8496                    }
8497                    if selected_package_points.contains_key(&region.sheet) {
8498                        package.source_report = Default::default();
8499                    }
8500                    let points = package.selected_points.as_ref().unwrap();
8501                    if !points.is_empty() {
8502                        selected_package_points
8503                            .entry(region.sheet.clone())
8504                            .or_default()
8505                            .extend(points.iter().copied());
8506                    }
8507                    package
8508                } else {
8509                    selected_package_sheets.insert(region.sheet.clone());
8510                    self.prepare_target_source_package(
8511                        &region.sheet,
8512                        package_lease,
8513                        options.deadline,
8514                    )?
8515                };
8516                if package
8517                    .selected_points
8518                    .as_ref()
8519                    .is_none_or(|points| !points.is_empty())
8520                {
8521                    let mut final_fallback = BTreeMap::new();
8522                    for record in package.fallback_records() {
8523                        final_fallback.insert((record.row, record.col), record.clone());
8524                    }
8525                    let batch = self.formula_batch_from_exact_replay(
8526                        &region.sheet,
8527                        final_fallback.into_values(),
8528                    )?;
8529                    for record in batch.formulas {
8530                        self.target_preparation_checkpoint(options.deadline, 1)?;
8531                        let ast = self
8532                            .graph
8533                            .data_store()
8534                            .retrieve_ast(record.ast_id, self.graph.sheet_reg())
8535                            .ok_or_else(|| {
8536                                ExcelError::new(ExcelErrorKind::Value)
8537                                    .with_message("target fallback AST is unavailable")
8538                            })?;
8539                        let snapshot =
8540                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8541                        if let Some(reason) =
8542                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8543                        {
8544                            return Err(Self::preparation_stale(
8545                                reason,
8546                                "target fallback planning snapshot became stale during discovery",
8547                            ));
8548                        }
8549                        let proven_sheet_local_dynamic =
8550                            Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
8551                        if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
8552                            if reason == OpaqueReason::DynamicReference
8553                                && proven_sheet_local_dynamic
8554                            {
8555                                Self::widen_target_preparation_to_sheet(
8556                                    options.opaque_policy,
8557                                    &mut scope,
8558                                    &mut reasons,
8559                                    reason,
8560                                    &region.sheet,
8561                                )?;
8562                            } else {
8563                                Self::widen_target_preparation(
8564                                    options.opaque_policy,
8565                                    &mut scope,
8566                                    &mut reasons,
8567                                    reason,
8568                                )?;
8569                            }
8570                        }
8571                        let placement = CellRef::new(
8572                            package.sheet_id,
8573                            Coord::from_excel(record.row, record.col, true, true),
8574                        );
8575                        let ingested = self
8576                            .graph
8577                            .ingest_pipeline(&snapshot)
8578                            .enable_function_semantics()
8579                            .ingest_formula(
8580                                FormulaAstInput::RawArena(record.ast_id),
8581                                placement,
8582                                record.formula_text,
8583                            )?;
8584                        if ingested.dep_plan.dynamic {
8585                            if proven_sheet_local_dynamic {
8586                                Self::widen_target_preparation_to_sheet(
8587                                    options.opaque_policy,
8588                                    &mut scope,
8589                                    &mut reasons,
8590                                    OpaqueReason::DynamicReference,
8591                                    &region.sheet,
8592                                )?;
8593                            } else {
8594                                Self::widen_target_preparation(
8595                                    options.opaque_policy,
8596                                    &mut scope,
8597                                    &mut reasons,
8598                                    OpaqueReason::DynamicReference,
8599                                )?;
8600                            }
8601                        }
8602                        for dep in &ingested.dep_plan.direct_cell_deps {
8603                            self.target_preparation_checkpoint(options.deadline, 1)?;
8604                            regions.push_back(PreparationRegion {
8605                                sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
8606                                sheet_id: dep.sheet_id,
8607                                start_row: dep.coord.row().saturating_add(1),
8608                                start_col: dep.coord.col().saturating_add(1),
8609                                end_row: dep.coord.row().saturating_add(1),
8610                                end_col: dep.coord.col().saturating_add(1),
8611                            });
8612                        }
8613                        for range in &ingested.dep_plan.range_deps {
8614                            self.target_preparation_checkpoint(options.deadline, 1)?;
8615                            // `Current` is the sheet the staged package's formula
8616                            // lives on.
8617                            let Ok(dependency_sheet) =
8618                                self.resolve_sheet_locator(&range.sheet, package.sheet_id)
8619                            else {
8620                                Self::widen_target_preparation(
8621                                    options.opaque_policy,
8622                                    &mut scope,
8623                                    &mut reasons,
8624                                    OpaqueReason::UnresolvedCrossSheetBinding,
8625                                )?;
8626                                continue;
8627                            };
8628                            regions.push_back(PreparationRegion {
8629                                sheet: self.graph.sheet_name(dependency_sheet).to_string(),
8630                                sheet_id: dependency_sheet,
8631                                start_row: range.start_row.map_or(1, |bound| bound.index + 1),
8632                                start_col: range.start_col.map_or(1, |bound| bound.index + 1),
8633                                end_row: range
8634                                    .end_row
8635                                    .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
8636                                        bound.index + 1
8637                                    }),
8638                                end_col: range
8639                                    .end_col
8640                                    .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
8641                                        bound.index + 1
8642                                    }),
8643                            });
8644                        }
8645                        for name in ingested
8646                            .dep_plan
8647                            .resolved_named_refs
8648                            .iter()
8649                            .chain(&ingested.dep_plan.named_refs)
8650                        {
8651                            self.target_preparation_checkpoint(options.deadline, 1)?;
8652                            if let Some(entry) =
8653                                self.graph.resolve_name_entry(name, package.sheet_id)
8654                            {
8655                                symbol_vertices.push_back(entry.vertex);
8656                            } else if self.graph.resolve_source_scalar_entry(name).is_none()
8657                                && self.graph.resolve_source_table_entry(name).is_none()
8658                            {
8659                                Self::widen_target_preparation(
8660                                    options.opaque_policy,
8661                                    &mut scope,
8662                                    &mut reasons,
8663                                    OpaqueReason::UnresolvedName,
8664                                )?;
8665                            }
8666                        }
8667                        for table in &ingested.dep_plan.table_refs {
8668                            self.target_preparation_checkpoint(options.deadline, 1)?;
8669                            if let Some(entry) = self.graph.resolve_table_entry(table) {
8670                                symbol_vertices.push_back(entry.vertex);
8671                            } else if self.graph.resolve_source_table_entry(table).is_none() {
8672                                Self::widen_target_preparation(
8673                                    options.opaque_policy,
8674                                    &mut scope,
8675                                    &mut reasons,
8676                                    OpaqueReason::UnresolvedTable,
8677                                )?;
8678                            }
8679                        }
8680                        package.legacy.push((
8681                            record.row,
8682                            record.col,
8683                            ingested.ast_id,
8684                            ingested.dep_plan,
8685                        ));
8686                    }
8687                    prepared_packages.push(package);
8688                }
8689            }
8690            let leases = self.staged_formula_index.leases_in_region(
8691                &region.sheet,
8692                region.start_row,
8693                region.start_col,
8694                region.end_row,
8695                region.end_col,
8696            );
8697            for lease in leases {
8698                self.target_preparation_checkpoint(options.deadline, 1)?;
8699                let sheet_id = self.graph.sheet_id(&region.sheet).ok_or_else(|| {
8700                    ExcelError::new(ExcelErrorKind::Ref)
8701                        .with_message(format!("staged formula sheet not found: {}", region.sheet))
8702                })?;
8703                let key = (region.sheet.clone(), lease.row, lease.col, lease.generation);
8704                if !selected.insert(key) {
8705                    continue;
8706                }
8707                let text = self
8708                    .staged_formulas
8709                    .get(&region.sheet)
8710                    .and_then(|sheet| sheet.get_ordinary(lease.row, lease.col))
8711                    .ok_or_else(|| {
8712                        ExcelError::new(ExcelErrorKind::Value)
8713                            .with_message("staged formula index is stale")
8714                    })?
8715                    .to_string();
8716                let formula = if text.starts_with('=') {
8717                    text.clone()
8718                } else {
8719                    format!("={text}")
8720                };
8721                self.target_preparation_checkpoint(options.deadline, 1)?;
8722                let ast = match formualizer_parse::parser::parse(&formula) {
8723                    Ok(ast) => ast,
8724                    Err(error) => {
8725                        if self.config.formula_parse_policy == FormulaParsePolicy::Strict {
8726                            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8727                                format!(
8728                                    "Formula parse error at {}!{}{}: {error}",
8729                                    region.sheet,
8730                                    col_letters_from_1based(lease.col)
8731                                        .unwrap_or_else(|_| "?".to_string()),
8732                                    lease.row
8733                                ),
8734                            ));
8735                        }
8736                        pending_diagnostics.push(FormulaParseDiagnostic {
8737                            sheet: region.sheet.clone(),
8738                            row: lease.row,
8739                            col: lease.col,
8740                            formula: formula.clone(),
8741                            message: error.to_string(),
8742                            policy: self.config.formula_parse_policy,
8743                        });
8744                        match self.config.formula_parse_policy {
8745                            FormulaParsePolicy::KeepCachedValue => {
8746                                selected_cells.push(
8747                                    formualizer_common::RangeAddress::new(
8748                                        region.sheet.clone(),
8749                                        lease.row,
8750                                        lease.col,
8751                                        lease.row,
8752                                        lease.col,
8753                                    )
8754                                    .expect("selected staged coordinates are valid"),
8755                                );
8756                                prepared.push(PreparedOrdinaryStagedFormula {
8757                                    sheet: region.sheet.clone(),
8758                                    sheet_id,
8759                                    lease,
8760                                    ast_id: None,
8761                                    plan: None,
8762                                });
8763                                continue;
8764                            }
8765                            FormulaParsePolicy::AsText => ASTNode::new(
8766                                ASTNodeType::Literal(LiteralValue::Text(formula.clone())),
8767                                None,
8768                            ),
8769                            FormulaParsePolicy::CoerceToError => ASTNode::new(
8770                                ASTNodeType::Literal(LiteralValue::Error(
8771                                    ExcelError::new(ExcelErrorKind::Error)
8772                                        .with_message(format!("Malformed formula: {error}")),
8773                                )),
8774                                None,
8775                            ),
8776                            FormulaParsePolicy::Strict => unreachable!(),
8777                        }
8778                    }
8779                };
8780                self.target_preparation_checkpoint(options.deadline, 1)?;
8781                let snapshot = self.target_planning_snapshot(&ast, &mut planning_requests)?;
8782                self.target_preparation_checkpoint(options.deadline, 1)?;
8783                if let Some(reason) =
8784                    Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8785                {
8786                    return Err(Self::preparation_stale(
8787                        reason,
8788                        "target planning snapshot became stale during discovery",
8789                    ));
8790                }
8791                if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
8792                    if reason == OpaqueReason::DynamicReference
8793                        && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
8794                    {
8795                        Self::widen_target_preparation_to_sheet(
8796                            options.opaque_policy,
8797                            &mut scope,
8798                            &mut reasons,
8799                            reason,
8800                            &region.sheet,
8801                        )?;
8802                    } else {
8803                        Self::widen_target_preparation(
8804                            options.opaque_policy,
8805                            &mut scope,
8806                            &mut reasons,
8807                            reason,
8808                        )?;
8809                    }
8810                }
8811                let proven_sheet_local_dynamic =
8812                    Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
8813                let placement = CellRef::new(
8814                    sheet_id,
8815                    Coord::from_excel(lease.row, lease.col, true, true),
8816                );
8817                let ingested = self.graph.ingest_pipeline(&snapshot).ingest_formula(
8818                    FormulaAstInput::Tree(ast),
8819                    placement,
8820                    Some(Arc::from(formula)),
8821                )?;
8822                self.target_preparation_checkpoint(options.deadline, 1)?;
8823                if ingested.dep_plan.dynamic {
8824                    if proven_sheet_local_dynamic {
8825                        Self::widen_target_preparation_to_sheet(
8826                            options.opaque_policy,
8827                            &mut scope,
8828                            &mut reasons,
8829                            OpaqueReason::DynamicReference,
8830                            &region.sheet,
8831                        )?;
8832                    } else {
8833                        Self::widen_target_preparation(
8834                            options.opaque_policy,
8835                            &mut scope,
8836                            &mut reasons,
8837                            OpaqueReason::DynamicReference,
8838                        )?;
8839                    }
8840                }
8841                for dep in &ingested.dep_plan.direct_cell_deps {
8842                    self.target_preparation_checkpoint(options.deadline, 1)?;
8843                    regions.push_back(PreparationRegion {
8844                        sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
8845                        sheet_id: dep.sheet_id,
8846                        start_row: dep.coord.row() + 1,
8847                        start_col: dep.coord.col() + 1,
8848                        end_row: dep.coord.row() + 1,
8849                        end_col: dep.coord.col() + 1,
8850                    });
8851                }
8852                for range in &ingested.dep_plan.range_deps {
8853                    self.target_preparation_checkpoint(options.deadline, 1)?;
8854                    // `Current` is the sheet the staged formula lives on.
8855                    let Ok(dependency_sheet) = self.resolve_sheet_locator(&range.sheet, sheet_id)
8856                    else {
8857                        Self::widen_target_preparation(
8858                            options.opaque_policy,
8859                            &mut scope,
8860                            &mut reasons,
8861                            OpaqueReason::UnresolvedCrossSheetBinding,
8862                        )?;
8863                        continue;
8864                    };
8865                    regions.push_back(PreparationRegion {
8866                        sheet: self.graph.sheet_name(dependency_sheet).to_string(),
8867                        sheet_id: dependency_sheet,
8868                        start_row: range.start_row.map_or(1, |bound| bound.index + 1),
8869                        start_col: range.start_col.map_or(1, |bound| bound.index + 1),
8870                        end_row: range
8871                            .end_row
8872                            .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
8873                                bound.index + 1
8874                            }),
8875                        end_col: range
8876                            .end_col
8877                            .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
8878                                bound.index + 1
8879                            }),
8880                    });
8881                }
8882                for name in ingested
8883                    .dep_plan
8884                    .resolved_named_refs
8885                    .iter()
8886                    .chain(&ingested.dep_plan.named_refs)
8887                {
8888                    self.target_preparation_checkpoint(options.deadline, 1)?;
8889                    if let Some(entry) = self.graph.resolve_name_entry(name, sheet_id) {
8890                        symbol_vertices.push_back(entry.vertex);
8891                    } else if self.graph.resolve_source_scalar_entry(name).is_none()
8892                        && self.graph.resolve_source_table_entry(name).is_none()
8893                    {
8894                        Self::widen_target_preparation(
8895                            options.opaque_policy,
8896                            &mut scope,
8897                            &mut reasons,
8898                            OpaqueReason::UnresolvedName,
8899                        )?;
8900                    }
8901                }
8902                for table in &ingested.dep_plan.table_refs {
8903                    self.target_preparation_checkpoint(options.deadline, 1)?;
8904                    if let Some(entry) = self.graph.resolve_table_entry(table) {
8905                        symbol_vertices.push_back(entry.vertex);
8906                    } else if self.graph.resolve_source_table_entry(table).is_none() {
8907                        Self::widen_target_preparation(
8908                            options.opaque_policy,
8909                            &mut scope,
8910                            &mut reasons,
8911                            OpaqueReason::UnresolvedTable,
8912                        )?;
8913                    }
8914                }
8915                selected_cells.push(
8916                    formualizer_common::RangeAddress::new(
8917                        region.sheet.clone(),
8918                        lease.row,
8919                        lease.col,
8920                        lease.row,
8921                        lease.col,
8922                    )
8923                    .expect("selected staged coordinates are valid"),
8924                );
8925                prepared.push(PreparedOrdinaryStagedFormula {
8926                    sheet: region.sheet.clone(),
8927                    sheet_id,
8928                    lease,
8929                    ast_id: Some(ingested.ast_id),
8930                    plan: Some(ingested.dep_plan),
8931                });
8932            }
8933
8934            if indexed_query_sheets.insert(region.sheet_id) {
8935                self.graph.prepare_sheet_index_for_query(region.sheet_id);
8936                let bytes = (self.graph.sheet_index_vertex_count(region.sheet_id) as u64)
8937                    .saturating_mul(32);
8938                self.reserve_graph_source_scratch(bytes)?;
8939                discovery_scratch_reserved = discovery_scratch_reserved.saturating_add(bytes);
8940            }
8941            let spill_anchors = self.graph.spill_anchors_in_region(
8942                region.sheet_id,
8943                region.start_row - 1,
8944                region.start_col - 1,
8945                region.end_row - 1,
8946                region.end_col - 1,
8947            );
8948            for anchor in spill_anchors {
8949                self.target_preparation_checkpoint(options.deadline, 1)?;
8950                symbol_vertices.push_back(anchor);
8951            }
8952            let vertices = self.graph.vertices_in_region(
8953                region.sheet_id,
8954                region.start_row - 1,
8955                region.end_row - 1,
8956                region.start_col - 1,
8957                region.end_col - 1,
8958            );
8959            for vertex in vertices {
8960                self.target_preparation_checkpoint(options.deadline, 1)?;
8961                symbol_vertices.push_back(vertex);
8962            }
8963        }
8964
8965        #[cfg(test)]
8966        self.target_preparation_fault(
8967            crate::engine::target_preparation::TargetPreparationFault::AfterDiscovery,
8968        )?;
8969
8970        if package_encountered {
8971            Self::widen_target_preparation(
8972                options.opaque_policy,
8973                &mut scope,
8974                &mut reasons,
8975                OpaqueReason::UnsupportedSourceSemantics,
8976            )?;
8977            self.target_preparation_checkpoint(options.deadline, 0)?;
8978            let selected_count = self.staged_formula_count();
8979            let selected_packages = self
8980                .staged_formulas
8981                .values()
8982                .filter_map(|staged| staged.deferred_package.as_ref())
8983                .map(|package| package.families.len() + package.partitioned_families.len())
8984                .sum();
8985            self.formula_parse_diagnostics.truncate(diagnostics_len);
8986            self.last_formula_ingest_report = report_len;
8987            #[cfg(test)]
8988            if let Some(hook) = self.before_target_preparation_commit_hook.take() {
8989                hook();
8990            }
8991            self.target_preparation_checkpoint(options.deadline, 0)?;
8992            #[cfg(test)]
8993            self.target_preparation_fault(
8994                crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
8995            )?;
8996            let current_revisions = self.preparation_revisions();
8997            if let Some(reason) = Self::preparation_revision_stale_reason(
8998                &assumptions,
8999                &current_revisions,
9000                &planning_requests,
9001                true,
9002            ) {
9003                return Err(Self::preparation_stale(
9004                    reason,
9005                    "target compatibility preparation plan is stale",
9006                ));
9007            }
9008            #[cfg(test)]
9009            self.target_preparation_fault(
9010                crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9011            )?;
9012            let commit_work_before = self
9013                .active_resource_ledger
9014                .as_ref()
9015                .map_or(0, |ledger| ledger.snapshot().work_charged);
9016            let commit_started = crate::instant::FzInstant::now();
9017            self.build_graph_all_unobserved()?;
9018            let commit_window = commit_started.elapsed();
9019            let ledger_after = self
9020                .active_resource_ledger
9021                .as_ref()
9022                .map(|ledger| ledger.snapshot());
9023            let actual_commit_work = ledger_after
9024                .map_or(0, |snapshot| snapshot.work_charged)
9025                .saturating_sub(commit_work_before);
9026            let observed_scratch_bytes = ledger_after
9027                .map_or(0, |snapshot| snapshot.scratch_peak)
9028                .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9029            let revisions = assumptions.clone();
9030            let report = PreparedTargetGraphReport {
9031                request_id: request_id.unwrap_or_default(),
9032                requested_targets: targets.len(),
9033                normalized_regions: visited_regions.len(),
9034                normalized_target_list: normalized,
9035                selected_staged_cells: selected_count,
9036                selected_source_families: selected_packages,
9037                retained_staged_cells: self.staged_formula_count(),
9038                selected_cells,
9039                retained_cells: Vec::new(),
9040                widened_scope: PrepareScope::Workbook,
9041                widening_reasons: reasons,
9042                revisions,
9043                commit_window,
9044                estimated_scratch_bytes: discovery_scratch_reserved
9045                    .saturating_add((selected_count as u64).saturating_mul(256)),
9046                observed_scratch_bytes,
9047                estimated_commit_work: selected_count as u64,
9048                actual_commit_work,
9049                outcome: PreparationOutcome::CompatibilityPrepared,
9050            };
9051            self.observe_target_preparation_report(&report);
9052            return Ok(report);
9053        }
9054
9055        prepared.sort_by_key(|formula| formula.lease.insertion_order);
9056        for package in &prepared_packages {
9057            if let Some(points) = &package.selected_points {
9058                selected_cells.extend(points.iter().filter_map(|&(row, col)| {
9059                    formualizer_common::RangeAddress::new(&package.sheet, row, col, row, col).ok()
9060                }));
9061                continue;
9062            }
9063            selected_cells.extend(package.replay_records.iter().filter_map(|record| {
9064                formualizer_common::RangeAddress::new(
9065                    package.sheet.clone(),
9066                    record.row,
9067                    record.col,
9068                    record.row,
9069                    record.col,
9070                )
9071                .ok()
9072            }));
9073        }
9074        let (legacy_graph, planned_formula_count) =
9075            self.prepare_target_combined_legacy_graph(&prepared_packages, &prepared)?;
9076        let new_vertices = legacy_graph.new_vertex_count();
9077        let new_edges = legacy_graph.planned_edge_count().ok_or_else(|| {
9078            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9079        })?;
9080        let removed_edges = legacy_graph.removed_edge_count().ok_or_else(|| {
9081            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9082        })?;
9083        let current = self.graph.baseline_stats();
9084        let final_vertices = current
9085            .graph_vertex_count
9086            .checked_add(new_vertices)
9087            .ok_or_else(|| {
9088                crate::engine::ResourceLedgerError::Exhausted(
9089                    formualizer_common::ResourceExhaustionDetail {
9090                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9091                        limit: u64::MAX,
9092                        observed: u64::MAX,
9093                        request_id,
9094                    },
9095                )
9096                .into_excel_error()
9097            })?;
9098        let final_edges = current
9099            .graph_edge_count
9100            .checked_sub(removed_edges)
9101            .and_then(|count| count.checked_add(new_edges))
9102            .ok_or_else(|| {
9103                crate::engine::ResourceLedgerError::Exhausted(
9104                    formualizer_common::ResourceExhaustionDetail {
9105                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9106                        limit: u64::MAX,
9107                        observed: u64::MAX,
9108                        request_id,
9109                    },
9110                )
9111                .into_excel_error()
9112            })?;
9113        #[cfg(test)]
9114        self.target_preparation_fault(
9115            crate::engine::target_preparation::TargetPreparationFault::Admission,
9116        )?;
9117        let resource = |reason, limit: u64, observed: u64| {
9118            crate::engine::ResourceLedgerError::Exhausted(
9119                formualizer_common::ResourceExhaustionDetail {
9120                    reason,
9121                    limit,
9122                    observed,
9123                    request_id,
9124                },
9125            )
9126            .into_excel_error()
9127        };
9128        let admission = crate::engine::resource_ledger::GraphAdmission {
9129            final_vertices,
9130            final_edges,
9131            materialization_cells: planned_formula_count as u64,
9132            added_vertices: new_vertices,
9133            added_edges: new_edges,
9134        };
9135        let materialized_bytes = admission
9136            .materialized_graph_bytes()
9137            .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
9138        if let Err(error) = self.preflight_graph_admission(admission) {
9139            if let formualizer_common::ExcelErrorExtra::Resource { detail } = &error.extra {
9140                self.observe_target_admission_failure(detail.reason);
9141            }
9142            return Err(error);
9143        }
9144        let selected_package_records = prepared_packages
9145            .iter()
9146            .map(|package| package.replay_records.len() as u64)
9147            .sum::<u64>();
9148        let planned_working_bytes = (prepared.len() as u64)
9149            .saturating_add(selected_package_records)
9150            .saturating_mul(256)
9151            .saturating_add((visited_regions.len() as u64).saturating_mul(128))
9152            .saturating_add((visited_vertices.len() as u64).saturating_mul(32))
9153            .saturating_add((selected.len() as u64).saturating_mul(96))
9154            .saturating_add(materialized_bytes);
9155        let residual_scratch = selected_package_points
9156            .iter()
9157            .map(|(sheet, points)| {
9158                let source = self
9159                    .staged_formulas
9160                    .get(sheet)
9161                    .unwrap()
9162                    .deferred_package
9163                    .as_ref()
9164                    .unwrap();
9165                (source.families.len() as u64)
9166                    .saturating_mul(256)
9167                    .saturating_add(
9168                        source
9169                            .partitioned_families
9170                            .iter()
9171                            .map(|family| {
9172                                256u64
9173                                    .saturating_add(family.fragments.len() as u64 * 32)
9174                                    .saturating_add(family.legacy_members.len() as u64 * 32)
9175                            })
9176                            .sum::<u64>(),
9177                    )
9178                    // Direct complete domains need coordinate suppression, not
9179                    // a per-member residual split/proof or legacy AST reserve.
9180                    .saturating_add(points.len() as u64 * 32)
9181                    .saturating_add(
9182                        prepared_packages
9183                            .iter()
9184                            .filter(|package| &package.sheet == sheet)
9185                            .map(|package| package.replay_records.len() as u64 * 480)
9186                            .sum::<u64>(),
9187                    )
9188            })
9189            .sum::<u64>();
9190        let scratch_bytes = discovery_scratch_reserved
9191            .saturating_add(planned_working_bytes)
9192            .saturating_add(residual_scratch);
9193        let remaining_scratch = scratch_bytes.saturating_sub(discovery_scratch_reserved);
9194        if let Err(error) = self.reserve_graph_source_scratch(remaining_scratch) {
9195            self.observe_target_admission_failure(
9196                formualizer_common::ResourceExhaustionReason::ScratchMemory,
9197            );
9198            return Err(error);
9199        }
9200
9201        let mut residual_sources = BTreeMap::new();
9202        let mut residual_owners = BTreeMap::new();
9203        for (sheet, points) in &selected_package_points {
9204            let selected: BTreeMap<_, BTreeSet<_>> = prepared_packages
9205                .iter()
9206                .filter(|package| &package.sheet == sheet)
9207                .flat_map(|package| &package.replay_records)
9208                .filter_map(|record| {
9209                    record.partition_owner.or(record.family).map(|owner| {
9210                        (
9211                            owner,
9212                            crate::engine::SourceCoord {
9213                                row: record.row - 1,
9214                                col: record.col - 1,
9215                            },
9216                        )
9217                    })
9218                })
9219                .fold(BTreeMap::new(), |mut map, (owner, coord)| {
9220                    map.entry(owner).or_default().insert(coord);
9221                    map
9222                });
9223            let metadata_work = self
9224                .staged_formulas
9225                .get(sheet)
9226                .unwrap()
9227                .deferred_package
9228                .as_ref()
9229                .map_or(0, |source| {
9230                    source.families.len() as u64
9231                        + source
9232                            .partitioned_families
9233                            .iter()
9234                            .map(|family| {
9235                                1 + family.fragments.len() as u64 * family.fragments.len() as u64
9236                                    + family.legacy_members.len() as u64
9237                            })
9238                            .sum::<u64>()
9239                });
9240            let split_work = selected
9241                .values()
9242                .map(|points| points.len() as u64 * 128)
9243                .sum::<u64>();
9244            self.target_preparation_checkpoint(
9245                options.deadline,
9246                metadata_work
9247                    .saturating_add(split_work)
9248                    .saturating_add(points.len() as u64),
9249            )?;
9250            let source = self
9251                .staged_formulas
9252                .get(sheet)
9253                .unwrap()
9254                .deferred_package
9255                .as_ref()
9256                .unwrap();
9257            let complete: BTreeSet<_> = prepared_packages
9258                .iter()
9259                .filter(|package| &package.sheet == sheet)
9260                .flat_map(|package| package.complete_selections.iter().copied())
9261                .collect();
9262            let residual = source
9263                .residual_sources(&selected, &complete, &self.workbook_load_limits)
9264                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
9265            residual_owners.insert(
9266                sheet.clone(),
9267                residual
9268                    .1
9269                    .iter()
9270                    .map(|family| family.source_id)
9271                    .collect::<BTreeSet<_>>(),
9272            );
9273            residual_sources.insert(sheet.clone(), residual);
9274        }
9275
9276        // Reserve residual suppression before the revision-validated commit window.
9277        // Hash-set insertion and point-index removal below cannot allocate.
9278        for (sheet, points) in &selected_package_points {
9279            let package = self
9280                .staged_formulas
9281                .get_mut(sheet)
9282                .unwrap()
9283                .deferred_package
9284                .as_mut()
9285                .unwrap();
9286            let owner_count = prepared_packages
9287                .iter()
9288                .filter(|p| &p.sheet == sheet)
9289                .flat_map(|p| &p.replay_records)
9290                .filter_map(|record| record.partition_owner.or(record.family))
9291                .filter(|owner| residual_owners[sheet].contains(owner))
9292                .count();
9293            package
9294                .consumed_members
9295                .try_reserve(owner_count)
9296                .map_err(|_| {
9297                    resource(
9298                        formualizer_common::ResourceExhaustionReason::ScratchMemory,
9299                        0,
9300                        owner_count as u64 * 24,
9301                    )
9302                })?;
9303            package.suppressed.try_reserve(points.len()).map_err(|_| {
9304                resource(
9305                    formualizer_common::ResourceExhaustionReason::ScratchMemory,
9306                    0,
9307                    points.len() as u64 * 16,
9308                )
9309            })?;
9310        }
9311
9312        let estimated_commit_duration = std::time::Duration::from_nanos(
9313            (new_vertices as u64)
9314                .saturating_add(new_edges as u64)
9315                .saturating_add(prepared.len() as u64)
9316                .saturating_add(selected_package_records)
9317                .max(1)
9318                .saturating_mul(100),
9319        );
9320        if options.deadline.is_some_and(|deadline| {
9321            std::time::Instant::now()
9322                .checked_add(estimated_commit_duration)
9323                .is_none_or(|finish| finish > deadline)
9324        }) {
9325            self.observe_target_admission_failure(
9326                formualizer_common::ResourceExhaustionReason::Deadline,
9327            );
9328            return Err(resource(
9329                formualizer_common::ResourceExhaustionReason::Deadline,
9330                0,
9331                1,
9332            ));
9333        }
9334        #[cfg(test)]
9335        if let Some(hook) = self.before_target_preparation_commit_hook.take() {
9336            hook();
9337        }
9338        self.target_preparation_checkpoint(options.deadline, 0)?;
9339        #[cfg(test)]
9340        self.target_preparation_fault(
9341            crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
9342        )?;
9343        let current_revisions = self.preparation_revisions();
9344        let staged_leases_match = prepared.iter().all(|formula| {
9345            self.staged_formula_index
9346                .lease_matches(&formula.sheet, formula.lease)
9347        }) && prepared_packages.iter().all(|package| {
9348            self.staged_formula_index
9349                .package_lease_matches(&package.sheet, package.lease)
9350        });
9351        let stale_reason = Self::preparation_revision_stale_reason(
9352            &assumptions,
9353            &current_revisions,
9354            &planning_requests,
9355            staged_leases_match,
9356        );
9357        if let Some(reason) = stale_reason {
9358            return Err(Self::preparation_stale(
9359                reason,
9360                "target graph preparation plan is stale",
9361            ));
9362        }
9363        #[cfg(test)]
9364        self.target_preparation_fault(
9365            crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9366        )?;
9367        self.graph
9368            .validate_prepared_legacy_graph_plan(&legacy_graph)
9369            .map_err(|error| {
9370                Self::preparation_stale(
9371                    formualizer_common::PreparationStaleReason::Graph,
9372                    format!("target graph preparation plan is stale: {error}"),
9373                )
9374            })?;
9375        #[cfg(test)]
9376        self.target_preparation_fault(
9377            crate::engine::target_preparation::TargetPreparationFault::Reservation,
9378        )?;
9379        self.graph.reserve_prepared_legacy_graph_plan(&legacy_graph);
9380        self.formula_parse_diagnostics
9381            .try_reserve(pending_diagnostics.len())
9382            .map_err(|_| {
9383                resource(
9384                    formualizer_common::ResourceExhaustionReason::Admission,
9385                    pending_diagnostics.len() as u64,
9386                    pending_diagnostics.len() as u64,
9387                )
9388            })?;
9389        self.target_preparation_checkpoint(options.deadline, 0)?;
9390        #[cfg(test)]
9391        self.target_preparation_fault(
9392            crate::engine::target_preparation::TargetPreparationFault::BeforeFirstMutation,
9393        )?;
9394
9395        let commit_started = crate::instant::FzInstant::now();
9396        let committed = self
9397            .graph
9398            .apply_prevalidated_legacy_graph_plan(legacy_graph);
9399        for formula in &prepared {
9400            let removed = self
9401                .staged_formulas
9402                .get_mut(&formula.sheet)
9403                .and_then(|sheet| sheet.remove_ordinary(formula.lease.row, formula.lease.col));
9404            debug_assert!(removed.is_some());
9405            let index_removed = self.staged_formula_index.remove(
9406                &formula.sheet,
9407                formula.lease.row,
9408                formula.lease.col,
9409            );
9410            debug_assert!(index_removed);
9411        }
9412        for package in &prepared_packages {
9413            if let Some(points) = &package.selected_points {
9414                let staged = self.staged_formulas.get_mut(&package.sheet).unwrap();
9415                let source = staged.deferred_package.as_mut().unwrap();
9416                source.consumed_engine = Some(Arc::clone(&self.source_formula_token));
9417                // The revision-validated graph commit establishes this exact source
9418                // ownership proof. Later edits retain the exclusion, not old text.
9419                source
9420                    .consumed_members
9421                    .extend(package.replay_records.iter().filter_map(|record| {
9422                        record
9423                            .partition_owner
9424                            .or(record.family)
9425                            .filter(|owner| residual_owners[&package.sheet].contains(owner))
9426                            .map(|owner| {
9427                                (
9428                                    owner,
9429                                    crate::engine::SourceCoord {
9430                                        row: record.row - 1,
9431                                        col: record.col - 1,
9432                                    },
9433                                )
9434                            })
9435                    }));
9436                source.suppressed.extend(points.iter().copied());
9437                source.source_accounted = true;
9438                self.staged_formula_index
9439                    .consume_package_points(&package.sheet, points);
9440                if source.suppressed.len() >= source.source_coordinates.len() {
9441                    staged.deferred_package = None;
9442                    self.staged_formula_index.set_package(&package.sheet, None);
9443                }
9444            } else {
9445                let removed = self
9446                    .staged_formulas
9447                    .get_mut(&package.sheet)
9448                    .and_then(|staged| staged.deferred_package.take());
9449                debug_assert!(removed.is_some());
9450                self.staged_formula_index.set_package(&package.sheet, None);
9451            }
9452        }
9453        for (sheet, (families, partitions)) in residual_sources {
9454            if let Some(source) = self
9455                .staged_formulas
9456                .get_mut(&sheet)
9457                .and_then(|staged| staged.deferred_package.as_mut())
9458            {
9459                source.families = families;
9460                source.partitioned_families = partitions;
9461                source
9462                    .consumed_members
9463                    .retain(|(owner, _)| residual_owners[&sheet].contains(owner));
9464                self.staged_formula_index.update_package_family_count(
9465                    &sheet,
9466                    source.families.len() + source.partitioned_families.len(),
9467                );
9468            }
9469        }
9470        let empty_sheets = self
9471            .staged_formulas
9472            .iter()
9473            .filter_map(|(sheet, staged)| staged.is_empty().then_some(sheet.clone()))
9474            .collect::<Vec<_>>();
9475        for sheet in empty_sheets {
9476            self.staged_formulas.remove(&sheet);
9477        }
9478        if committed > 0 {
9479            self.mark_topology_edited();
9480        }
9481        self.formula_parse_diagnostics.extend(pending_diagnostics);
9482        if !prepared.is_empty() || !prepared_packages.is_empty() {
9483            let mut ingest_delta = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
9484            ingest_delta.formula_cells_seen = (prepared.len() as u64).saturating_add(
9485                prepared_packages
9486                    .iter()
9487                    .map(|package| {
9488                        (package.replay_records.len() as u64).saturating_add(
9489                            if package.selected_points.is_some() {
9490                                package.direct_cells
9491                            } else {
9492                                0
9493                            },
9494                        )
9495                    })
9496                    .sum::<u64>(),
9497            );
9498            ingest_delta.graph_formula_cells_materialized = committed as u64;
9499            ingest_delta.graph_vertices_created = new_vertices as u64;
9500            ingest_delta.graph_edges_created = new_edges as u64;
9501            for package in &prepared_packages {
9502                let source = &package.source_report;
9503                ingest_delta.source_formula_events = ingest_delta
9504                    .source_formula_events
9505                    .saturating_add(source.source_formula_events);
9506                ingest_delta.source_formula_records_spooled = ingest_delta
9507                    .source_formula_records_spooled
9508                    .saturating_add(source.source_formula_records_spooled);
9509                ingest_delta.source_spool_encoded_bytes = ingest_delta
9510                    .source_spool_encoded_bytes
9511                    .saturating_add(source.source_spool_encoded_bytes);
9512                ingest_delta.source_spool_peak_memory_bytes = ingest_delta
9513                    .source_spool_peak_memory_bytes
9514                    .max(source.source_spool_peak_memory_bytes);
9515                ingest_delta.source_spool_spilled_bytes = ingest_delta
9516                    .source_spool_spilled_bytes
9517                    .saturating_add(source.source_spool_spilled_bytes);
9518                ingest_delta.source_spool_spill_files = ingest_delta
9519                    .source_spool_spill_files
9520                    .saturating_add(source.source_spool_spill_files);
9521                ingest_delta.source_spool_replays = ingest_delta
9522                    .source_spool_replays
9523                    .saturating_add(source.source_spool_replays)
9524                    .saturating_add(package.spool_replays);
9525                ingest_delta.source_families_seen = ingest_delta
9526                    .source_families_seen
9527                    .saturating_add(source.families_seen);
9528                ingest_delta.source_family_cells_seen = ingest_delta
9529                    .source_family_cells_seen
9530                    .saturating_add(source.family_cells_seen);
9531                ingest_delta.source_family_shadow_eligible = ingest_delta
9532                    .source_family_shadow_eligible
9533                    .saturating_add(source.source_clean_families);
9534                ingest_delta.source_family_shadow_eligible_cells = ingest_delta
9535                    .source_family_shadow_eligible_cells
9536                    .saturating_add(source.source_clean_cells);
9537                ingest_delta.source_partitioned_families_seen = ingest_delta
9538                    .source_partitioned_families_seen
9539                    .saturating_add(source.source_fragmentable_families);
9540                ingest_delta.source_partition_holes = ingest_delta
9541                    .source_partition_holes
9542                    .saturating_add(source.source_hole_exclusions);
9543                ingest_delta.source_partition_ordinary_exceptions = ingest_delta
9544                    .source_partition_ordinary_exceptions
9545                    .saturating_add(source.source_ordinary_exclusions);
9546                ingest_delta.source_partition_surviving_cells = ingest_delta
9547                    .source_partition_surviving_cells
9548                    .saturating_add(source.source_fragmentable_cells);
9549                for (reason, count) in &source.fallback_reasons {
9550                    let total = ingest_delta
9551                        .fallback_reasons
9552                        .entry(reason.clone())
9553                        .or_default();
9554                    *total = total.saturating_add(*count);
9555                }
9556
9557                ingest_delta.source_family_fallback = ingest_delta
9558                    .source_family_fallback
9559                    .saturating_add(source.families_seen);
9560                ingest_delta.source_family_fallback_cells = ingest_delta
9561                    .source_family_fallback_cells
9562                    .saturating_add(source.family_cells_seen);
9563            }
9564            self.record_formula_ingest_report(ingest_delta);
9565        }
9566        let commit_window = commit_started.elapsed();
9567        let retained_cells = self
9568            .staged_formula_index
9569            .all_leases()
9570            .into_iter()
9571            .filter_map(|(sheet, lease)| {
9572                formualizer_common::RangeAddress::new(
9573                    sheet, lease.row, lease.col, lease.row, lease.col,
9574                )
9575                .ok()
9576            })
9577            .collect::<Vec<_>>();
9578        let observed_scratch_bytes = self
9579            .active_resource_ledger
9580            .as_ref()
9581            .map_or(0, |ledger| ledger.snapshot().scratch_peak)
9582            .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9583        let committed_spans = 0u64;
9584        let selected_source_families = prepared_packages
9585            .iter()
9586            .filter(|package| package.selected_points.is_none())
9587            .map(|package| package.lease.family_count)
9588            .sum::<usize>()
9589            + prepared_packages
9590                .iter()
9591                .filter(|package| package.selected_points.is_some())
9592                .flat_map(|package| package.replay_records.iter())
9593                .filter_map(|record| record.partition_owner.or(record.family))
9594                .chain(
9595                    prepared_packages
9596                        .iter()
9597                        .flat_map(|package| package.complete_selections.iter().copied()),
9598                )
9599                .collect::<BTreeSet<_>>()
9600                .len();
9601        let selected_staged_cells = prepared.len().saturating_add(
9602            prepared_packages
9603                .iter()
9604                .map(|package| {
9605                    package
9606                        .selected_points
9607                        .as_ref()
9608                        .map_or(package.replay_records.len(), BTreeSet::len)
9609                })
9610                .sum::<usize>(),
9611        );
9612        let actual_commit_work = (new_vertices as u64)
9613            .saturating_add(new_edges as u64)
9614            .saturating_add(committed as u64)
9615            .saturating_add(committed_spans)
9616            .saturating_add(prepared.len() as u64)
9617            .saturating_add(
9618                prepared_packages
9619                    .iter()
9620                    .map(|package| {
9621                        package
9622                            .selected_points
9623                            .as_ref()
9624                            .map_or(1, |points| points.len() as u64)
9625                    })
9626                    .sum::<u64>(),
9627            );
9628        let report = PreparedTargetGraphReport {
9629            request_id: request_id.unwrap_or_default(),
9630            requested_targets: targets.len(),
9631            normalized_regions: visited_regions.len(),
9632            normalized_target_list: normalized,
9633            selected_staged_cells,
9634            selected_source_families,
9635            retained_staged_cells: self.staged_formula_count(),
9636            selected_cells,
9637            retained_cells,
9638            widened_scope: scope,
9639            widening_reasons: reasons,
9640            revisions: assumptions,
9641            commit_window,
9642            estimated_scratch_bytes: scratch_bytes,
9643            observed_scratch_bytes,
9644            estimated_commit_work: (new_vertices as u64)
9645                .saturating_add(new_edges as u64)
9646                .saturating_add(prepared.len() as u64)
9647                .saturating_add(selected_package_records),
9648            actual_commit_work,
9649            outcome: PreparationOutcome::Prepared,
9650        };
9651        self.observe_target_preparation_report(&report);
9652        Ok(report)
9653    }
9654
9655    /// Build graph for all staged formulas.
9656    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9657        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
9658            engine.build_graph_all_unobserved()
9659        })
9660    }
9661
9662    fn build_graph_all_unobserved(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9663        let selected = self.staged_formula_count();
9664        let started = crate::instant::FzInstant::now();
9665        self.resource_checkpoint(selected as u64)?;
9666        let scratch_bytes = (selected as u64).saturating_mul(256);
9667        let result = self.with_request_scratch(scratch_bytes, |engine| {
9668            let index_snapshot = engine.staged_formula_index.clone();
9669            let collected = std::mem::take(&mut engine.staged_formulas)
9670                .into_iter()
9671                .collect();
9672            engine.staged_formula_index.clear_all();
9673            engine.build_graph_from_staged_batches(collected, false, index_snapshot)
9674        });
9675        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9676        result
9677    }
9678
9679    /// Build graph for specific sheets (consuming only those staged entries).
9680    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
9681        &mut self,
9682        sheets: I,
9683    ) -> Result<(), formualizer_parse::ExcelError> {
9684        let mut sheets = sheets.into_iter();
9685        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, move |engine| {
9686            let name_scratch = (sheets.size_hint().0 as u64).saturating_mul(64);
9687            engine.with_request_scratch(name_scratch, |engine| {
9688                // Allocation failure follows the baseline process-fatal policy; it is not a
9689                // recoverable resource error or a new staged-preparation route.
9690                let names = sheets.by_ref().map(str::to_string).collect::<Vec<_>>();
9691                engine.charge_bounded_work(names.len() as u64)?;
9692                engine.build_graph_for_sheet_names_unobserved(names)
9693            })
9694        })
9695    }
9696
9697    fn build_graph_for_sheet_names_unobserved(
9698        &mut self,
9699        sheets: Vec<String>,
9700    ) -> Result<(), formualizer_parse::ExcelError> {
9701        let started = crate::instant::FzInstant::now();
9702        let selected = sheets
9703            .iter()
9704            .filter_map(|sheet| self.staged_formulas.get(sheet))
9705            .map(StagedSheet::len)
9706            .sum::<usize>();
9707        self.resource_checkpoint(selected as u64)?;
9708        let scratch_bytes = (selected as u64).saturating_mul(256);
9709        self.reserve_request_scratch(scratch_bytes)?;
9710        let index_snapshot = self.staged_formula_index.clone();
9711        let mut collected = Vec::new();
9712        for sheet in sheets {
9713            if let Some(staged) = self.staged_formulas.remove(&sheet) {
9714                self.index_removed_staged_sheet(&sheet, &staged);
9715                collected.push((sheet, staged));
9716            }
9717        }
9718        let result = self.build_graph_from_staged_batches(collected, true, index_snapshot);
9719        self.release_request_scratch(scratch_bytes);
9720        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9721        result
9722    }
9723
9724    fn build_graph_from_staged_batches(
9725        &mut self,
9726        collected: StagedFormulaBatches,
9727        share_parse_cache_across_sheets: bool,
9728        staged_index_snapshot: StagedFormulaIndex,
9729    ) -> Result<(), formualizer_parse::ExcelError> {
9730        if collected.is_empty() {
9731            return Ok(());
9732        }
9733        for (sheet, _) in &collected {
9734            let _ = self.add_sheet(sheet);
9735        }
9736
9737        let diagnostics_len = self.formula_parse_diagnostics.len();
9738        let mut collected = collected;
9739        let prepared = match self
9740            .prepare_staged_formula_batches(&mut collected, share_parse_cache_across_sheets)
9741        {
9742            Ok(prepared) => prepared,
9743            Err(error) => {
9744                self.formula_parse_diagnostics.truncate(diagnostics_len);
9745                for (sheet, staged) in collected {
9746                    self.restore_staged_sheet(sheet, staged);
9747                }
9748                self.staged_formula_index = staged_index_snapshot;
9749                return Err(error);
9750            }
9751        };
9752        let (ordinary, compressed, direct, may_fail) = prepared;
9753
9754        // A first build (no formula in the graph yet) goes through the eager
9755        // first load's machinery: the builder pre-allocates each column's
9756        // targets as one id run, installs family members as virtual runs and
9757        // builds the authority once at the end (decision 26).
9758        // The first load's builder plans and applies one chunk at a time, so
9759        // a planning error would leave earlier chunks in the graph; the
9760        // incremental path plans everything first. Check every distinct
9761        // formula first, in the incremental path's order (it reports the
9762        // same first error and leaves the graph untouched).
9763        let first_build = if self.deferred_build_can_be_first_load() {
9764            if let Err(error) = self.check_staged_formula_plans(
9765                ordinary
9766                    .iter()
9767                    .chain(compressed.iter().map(|(batch, _)| batch)),
9768                &may_fail,
9769            ) {
9770                self.formula_parse_diagnostics.truncate(diagnostics_len);
9771                for (sheet, staged) in collected {
9772                    self.restore_staged_sheet(sheet, staged);
9773                }
9774                self.staged_formula_index = staged_index_snapshot;
9775                return Err(error);
9776            }
9777            self.deferred_build_as_first_load()
9778        } else {
9779            None
9780        };
9781        let built_sheets: Vec<String> = if first_build.is_some() {
9782            collected.iter().map(|(sheet, _)| sheet.clone()).collect()
9783        } else {
9784            Vec::new()
9785        };
9786
9787        // Keep the original source/spool alive through every fallible ingestion route.
9788        // Graph admission may have committed a prefix; replay replaces those placements
9789        // rather than treating their cached values as authoritative source.
9790        let result = (|| {
9791            if !ordinary.is_empty() {
9792                self.ingest_formula_batches(ordinary)?;
9793            }
9794            if !compressed.is_empty() {
9795                self.ingest_compressed_formula_source_batches(compressed)?;
9796            }
9797            if !direct.is_empty() {
9798                self.finish_compressed_formula_sources(direct)?;
9799            }
9800            Ok(())
9801        })();
9802        if let Some(saved) = first_build {
9803            self.leave_deferred_first_load(saved, &built_sheets);
9804        }
9805        if let Err(error) = result {
9806            self.formula_parse_diagnostics.truncate(diagnostics_len);
9807            for (sheet, staged) in collected {
9808                self.restore_staged_sheet(sheet, staged);
9809            }
9810            self.staged_formula_index = staged_index_snapshot;
9811            return Err(error);
9812        }
9813        self.dedup_formula_parse_diagnostics_since(diagnostics_len);
9814        Ok(())
9815    }
9816
9817    /// Enter the first-load ingest mode for a deferred build when the graph
9818    /// holds no formula yet and no load is in progress: the settings the
9819    /// calamine loader uses for its eager first load (lazy sheet index, no
9820    /// small-range expansion, first-load fast path). Returns the settings
9821    /// to restore, or `None` when the build takes the incremental path.
9822    fn deferred_build_can_be_first_load(&self) -> bool {
9823        !self.graph.first_load_assume_new()
9824            && !self.graph_admission_enabled()
9825            && self.graph.formula_vertex_count() == 0
9826    }
9827
9828    /// Plan each distinct formula of `batches` that may fail planning once
9829    /// (a member is planned through its template, staged before it) and
9830    /// drop the plans: the first planning error, in the order the
9831    /// incremental ingest meets it.
9832    fn check_staged_formula_plans<'b>(
9833        &mut self,
9834        batches: impl Iterator<Item = &'b FormulaIngestBatch>,
9835        may_fail: &FxHashSet<crate::engine::arena::AstNodeId>,
9836    ) -> Result<(), ExcelError> {
9837        if may_fail.is_empty() {
9838            return Ok(());
9839        }
9840        let mut seen: FxHashSet<(SheetId, crate::engine::arena::AstNodeId)> = FxHashSet::default();
9841        for batch in batches {
9842            let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
9843                ExcelError::new(ExcelErrorKind::Ref)
9844                    .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
9845            })?;
9846            let mut pipeline = self.ingest_pipeline();
9847            for record in &batch.formulas {
9848                if record.member_anchor.is_some()
9849                    || !may_fail.contains(&record.ast_id)
9850                    || !seen.insert((sheet_id, record.ast_id))
9851                {
9852                    continue;
9853                }
9854                let placement = CellRef::new(
9855                    sheet_id,
9856                    Coord::from_excel(record.row, record.col, true, true),
9857                );
9858                pipeline.ingest_formula(
9859                    FormulaAstInput::RawArena(record.ast_id),
9860                    placement,
9861                    None,
9862                )?;
9863            }
9864        }
9865        Ok(())
9866    }
9867
9868    fn deferred_build_as_first_load(&mut self) -> Option<(crate::engine::SheetIndexMode, usize)> {
9869        if !self.deferred_build_can_be_first_load() {
9870            return None;
9871        }
9872        let saved = (
9873            self.graph.get_config().sheet_index_mode,
9874            self.config.range_expansion_limit,
9875        );
9876        self.graph
9877            .set_sheet_index_mode(crate::engine::SheetIndexMode::Lazy);
9878        self.config.range_expansion_limit = 0;
9879        self.graph.set_first_load_assume_new(true);
9880        self.graph.reset_ensure_touched();
9881        Some(saved)
9882    }
9883
9884    /// Leave the first-load mode of [`Self::deferred_build_as_first_load`]:
9885    /// the authority is built once here, as at the end of an eager load.
9886    fn leave_deferred_first_load(
9887        &mut self,
9888        (index_mode, range_limit): (crate::engine::SheetIndexMode, usize),
9889        sheets: &[String],
9890    ) {
9891        self.graph.set_first_load_assume_new(false);
9892        self.graph.reset_ensure_touched();
9893        self.graph.set_sheet_index_mode(index_mode);
9894        self.config.range_expansion_limit = range_limit;
9895        for sheet in sheets {
9896            self.graph.finalize_sheet_index(sheet);
9897        }
9898    }
9899
9900    fn prepare_staged_formula_batches(
9901        &mut self,
9902        collected: &mut StagedFormulaBatches,
9903        share_parse_cache_across_sheets: bool,
9904    ) -> Result<PreparedStagedFormulaBatches, formualizer_parse::ExcelError> {
9905        let mut ordinary = Vec::new();
9906        let mut compressed = Vec::new();
9907        let mut direct = Vec::new();
9908        let mut may_fail = FxHashSet::default();
9909        let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
9910            rustc_hash::FxHashMap::default();
9911        cache.reserve(4096);
9912
9913        for (sheet, staged) in collected {
9914            if !share_parse_cache_across_sheets {
9915                cache.clear();
9916            }
9917            // Load-time family grouping, as the eager first load does:
9918            // relative copies of the formula above (or to the left) become
9919            // members of its family and are never interned.
9920            let mut grouper = crate::engine::FormulaFamilyGrouper::new();
9921            // The staged texts, then the deferred package's replayed ones,
9922            // are walked once (no combined copy: a replay can be the whole
9923            // sheet).
9924            let mut replayed_records = Vec::new();
9925            let deferred_source = None;
9926            let mut deferred_fallback = None;
9927            if let Some(package) = staged.deferred_package.as_mut() {
9928                if package.sheet_name != *sheet {
9929                    return Err(ExcelError::new(ExcelErrorKind::Value)
9930                        .with_message("deferred formula package sheet mismatch"));
9931                }
9932                let eligible: Vec<_> = package
9933                    .families
9934                    .iter()
9935                    .filter(|family| !package.invalidated.contains(&family.source_id))
9936                    .cloned()
9937                    .collect();
9938                let eligible_partitions: Vec<_> = package
9939                    .partitioned_families
9940                    .iter()
9941                    .filter(|family| !package.invalidated.contains(&family.source_id))
9942                    .cloned()
9943                    .collect();
9944
9945                let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
9946                for partition in &eligible_partitions {
9947                    replay_disposition
9948                        .register_partition(partition, false)
9949                        .map_err(|reason| {
9950                            ExcelError::new(ExcelErrorKind::Value).with_message(reason)
9951                        })?;
9952                }
9953                replay_disposition
9954                    .extend_suppressed_excel_coords(package.suppressed.iter().copied());
9955                let replayed = package
9956                    .replay
9957                    .lock()
9958                    .map_err(|_| {
9959                        ExcelError::new(ExcelErrorKind::Value)
9960                            .with_message("deferred formula spool lock poisoned")
9961                    })?
9962                    .replay(&replay_disposition)
9963                    .map_err(|message| {
9964                        ExcelError::new(ExcelErrorKind::Value).with_message(message)
9965                    })?;
9966                replayed_records = replayed;
9967                let mut report = package.accounting_report();
9968                report.source_spool_replays = report.source_spool_replays.saturating_add(1);
9969                deferred_fallback = Some((report, package.families.clone(), eligible_partitions));
9970            }
9971
9972            let n_entries = staged.entries.len() + replayed_records.len();
9973            let entries = staged
9974                .entries
9975                .iter()
9976                .cloned()
9977                .map(|(row, col, text)| (row, col, text, None))
9978                .chain(replayed_records.into_iter().map(|record| {
9979                    (
9980                        record.row,
9981                        record.col,
9982                        record.text,
9983                        Some((record.source_order, record.family, record.partition_owner)),
9984                    )
9985                }));
9986            let mut formulas = Vec::with_capacity(n_entries);
9987            let staged_order_base = u64::MAX.saturating_sub(n_entries as u64);
9988            for (entry_index, (row, col, txt, source_proof)) in entries.enumerate() {
9989                let key = if txt.starts_with('=') {
9990                    txt
9991                } else {
9992                    format!("={txt}")
9993                };
9994                let staged_record = if let Some(cached) = cache.get(&key) {
9995                    cached.map(|ast_id| {
9996                        self.note_staged_formula(&mut grouper, row, col, ast_id);
9997                        FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(key)))
9998                    })
9999                } else {
10000                    let parsed = match formualizer_parse::parser::parse(&key) {
10001                        Ok(parsed) => Some(parsed),
10002                        Err(error) => self.handle_formula_parse_error(
10003                            sheet,
10004                            row,
10005                            col,
10006                            &key,
10007                            error.to_string(),
10008                        )?,
10009                    };
10010                    match parsed {
10011                        Some(ast) => {
10012                            let record = self.stage_formula_ast(&mut grouper, row, col, &ast, None);
10013                            if !record.is_family_member() && self.formula_may_fail_planning(&ast) {
10014                                may_fail.insert(record.ast_id);
10015                            }
10016                            // A member's text is not worth caching: relative
10017                            // copies do not repeat their text.
10018                            if record.is_family_member() {
10019                                Some(record)
10020                            } else {
10021                                let ast_id = record.ast_id;
10022                                cache.insert(key.clone(), Some(ast_id));
10023                                Some(FormulaIngestRecord::new(
10024                                    row,
10025                                    col,
10026                                    ast_id,
10027                                    Some(Arc::<str>::from(key)),
10028                                ))
10029                            }
10030                        }
10031                        None => {
10032                            cache.insert(key, None);
10033                            None
10034                        }
10035                    }
10036                };
10037
10038                if let Some(mut formula) = staged_record {
10039                    if let Some((order, family, owner)) = source_proof {
10040                        formula = formula.with_source_proof(order, family, owner);
10041                    } else if deferred_source.is_some() {
10042                        formula = formula.with_source_proof(
10043                            crate::engine::SourceFormulaOrder::new(
10044                                staged_order_base.saturating_add(entry_index as u64),
10045                            ),
10046                            None,
10047                            None,
10048                        );
10049                    }
10050                    formulas.push(formula);
10051                }
10052            }
10053
10054            let batch = FormulaIngestBatch::new(sheet.clone(), formulas);
10055            if let Some((report, preparation)) = deferred_source {
10056                direct.push((batch, report, preparation));
10057            } else if let Some((report, families, partitions)) = deferred_fallback {
10058                let source_batch = crate::engine::FormulaCompressedSourceBatch::with_proposals(
10059                    batch.sheet_name.clone(),
10060                    report,
10061                    families,
10062                    partitions,
10063                );
10064                compressed.push((batch, source_batch));
10065            } else if !batch.is_empty() {
10066                ordinary.push(batch);
10067            }
10068        }
10069        Ok((ordinary, compressed, direct, may_fail))
10070    }
10071
10072    /// Whether planning `ast` can fail: a reference to a sheet that does not
10073    /// exist, an external, 3-D or table reference, or a reversed range.
10074    /// Unqualified cell and range references, names, literals and calls
10075    /// always plan.
10076    fn formula_may_fail_planning(&self, ast: &formualizer_parse::parser::ASTNode) -> bool {
10077        use formualizer_parse::parser::{ASTNodeType, ReferenceType};
10078        let sheet_missing = |sheet: &Option<String>| {
10079            sheet
10080                .as_deref()
10081                .is_some_and(|s| self.graph.sheet_id(s).is_none())
10082        };
10083        match &ast.node_type {
10084            ASTNodeType::Literal(_) | ASTNodeType::Omitted => false,
10085            ASTNodeType::Reference { reference, .. } => match reference {
10086                ReferenceType::Cell { sheet, .. } => sheet_missing(sheet),
10087                ReferenceType::Range {
10088                    sheet,
10089                    start_row,
10090                    start_col,
10091                    end_row,
10092                    end_col,
10093                    ..
10094                } => {
10095                    sheet_missing(sheet)
10096                        || matches!((start_row, end_row), (Some(a), Some(b)) if a > b)
10097                        || matches!((start_col, end_col), (Some(a), Some(b)) if a > b)
10098                }
10099                ReferenceType::NamedRange(_) => false,
10100                _ => true,
10101            },
10102            ASTNodeType::UnaryOp { expr, .. } => self.formula_may_fail_planning(expr),
10103            ASTNodeType::BinaryOp { left, right, .. } => {
10104                self.formula_may_fail_planning(left) || self.formula_may_fail_planning(right)
10105            }
10106            ASTNodeType::Function { args, .. } => {
10107                args.iter().any(|a| self.formula_may_fail_planning(a))
10108            }
10109            ASTNodeType::Call { callee, args } => {
10110                self.formula_may_fail_planning(callee)
10111                    || args.iter().any(|a| self.formula_may_fail_planning(a))
10112            }
10113            ASTNodeType::Array(rows) => rows
10114                .iter()
10115                .flatten()
10116                .any(|a| self.formula_may_fail_planning(a)),
10117        }
10118    }
10119
10120    /// Begin bulk Arrow ingest for base values (Phase A)
10121    pub fn begin_bulk_ingest_arrow(
10122        &mut self,
10123    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
10124        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
10125    }
10126
10127    /// Begin bulk updates to Arrow store (Phase C)
10128    pub fn begin_bulk_update_arrow(
10129        &mut self,
10130    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
10131        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
10132    }
10133
10134    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
10135        self.graph.sheet_id(sheet).ok_or(
10136            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10137                name: sheet.to_string(),
10138                reason: "Unknown sheet".to_string(),
10139            },
10140        )
10141    }
10142
10143    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
10144        if row_1based == 0 {
10145            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10146        }
10147        Ok(row_1based - 1)
10148    }
10149
10150    fn normalize_row_range_1based(
10151        start_row_1based: u32,
10152        end_row_1based: u32,
10153    ) -> Result<(u32, u32), crate::engine::EditorError> {
10154        if start_row_1based == 0 || end_row_1based == 0 {
10155            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10156        }
10157        if start_row_1based > end_row_1based {
10158            return Err(crate::engine::EditorError::TransactionFailed {
10159                reason: "Row range start is greater than end".to_string(),
10160            });
10161        }
10162        Ok((start_row_1based - 1, end_row_1based - 1))
10163    }
10164
10165    fn invalidate_row_visibility_mask_cache(&self) {
10166        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10167            cache.clear();
10168        }
10169    }
10170
10171    fn set_row_hidden_by_sheet_id(
10172        &mut self,
10173        sheet_id: SheetId,
10174        row0: u32,
10175        hidden: bool,
10176        source: RowVisibilitySource,
10177    ) -> bool {
10178        let changed = {
10179            let state = self.row_visibility.entry(sheet_id).or_default();
10180            state.set_row_hidden(row0, hidden, source)
10181        };
10182
10183        let remove_entry = self
10184            .row_visibility
10185            .get(&sheet_id)
10186            .map(|state| state.is_empty())
10187            .unwrap_or(false);
10188        if remove_entry {
10189            self.row_visibility.remove(&sheet_id);
10190        }
10191
10192        if changed {
10193            self.invalidate_row_visibility_mask_cache();
10194        }
10195
10196        changed
10197    }
10198
10199    fn set_rows_hidden_by_sheet_id(
10200        &mut self,
10201        sheet_id: SheetId,
10202        start_row0: u32,
10203        end_row0: u32,
10204        hidden: bool,
10205        source: RowVisibilitySource,
10206    ) -> bool {
10207        let changed = {
10208            let state = self.row_visibility.entry(sheet_id).or_default();
10209            state.set_rows_hidden(start_row0, end_row0, hidden, source)
10210        };
10211
10212        let remove_entry = self
10213            .row_visibility
10214            .get(&sheet_id)
10215            .map(|state| state.is_empty())
10216            .unwrap_or(false);
10217        if remove_entry {
10218            self.row_visibility.remove(&sheet_id);
10219        }
10220
10221        if changed {
10222            self.invalidate_row_visibility_mask_cache();
10223        }
10224
10225        changed
10226    }
10227
10228    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
10229        if count == 0 {
10230            return;
10231        }
10232        let mut changed = false;
10233        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10234            changed = state.insert_rows(before0, count);
10235            state.is_empty()
10236        } else {
10237            false
10238        };
10239        if remove_entry {
10240            self.row_visibility.remove(&sheet_id);
10241        }
10242        if changed {
10243            self.invalidate_row_visibility_mask_cache();
10244        }
10245    }
10246
10247    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
10248        if count == 0 {
10249            return;
10250        }
10251        let mut changed = false;
10252        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10253            changed = state.delete_rows(start0, count);
10254            state.is_empty()
10255        } else {
10256            false
10257        };
10258        if remove_entry {
10259            self.row_visibility.remove(&sheet_id);
10260        }
10261        if changed {
10262            self.invalidate_row_visibility_mask_cache();
10263        }
10264    }
10265
10266    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10267        if let crate::engine::ChangeEvent::SetRowVisibility {
10268            sheet_id,
10269            row0,
10270            source,
10271            old_hidden,
10272            ..
10273        } = event
10274        {
10275            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
10276        }
10277    }
10278
10279    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10280        if let crate::engine::ChangeEvent::SetRowVisibility {
10281            sheet_id,
10282            row0,
10283            source,
10284            new_hidden,
10285            ..
10286        } = event
10287        {
10288            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
10289        }
10290    }
10291
10292    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10293        for event in events.iter().rev() {
10294            self.apply_inverse_row_visibility_event(event);
10295        }
10296    }
10297
10298    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10299        for event in events {
10300            self.apply_forward_row_visibility_event(event);
10301        }
10302    }
10303
10304    fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10305        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10306            sheet,
10307            row,
10308            col,
10309            old,
10310            ..
10311        } = event
10312        {
10313            self.apply_staged_formula_cell(sheet, *row, *col, old.as_deref());
10314        }
10315    }
10316
10317    fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10318        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10319            sheet,
10320            row,
10321            col,
10322            new,
10323            ..
10324        } = event
10325        {
10326            self.apply_staged_formula_cell(sheet, *row, *col, new.as_deref());
10327        }
10328    }
10329
10330    /// Set a single cell's staged formula text to `target` (clearing it when
10331    /// `None`). Used by undo/redo replay of per-cell staged-formula deltas.
10332    fn apply_staged_formula_cell(&mut self, sheet: &str, row: u32, col: u32, target: Option<&str>) {
10333        match target {
10334            Some(text) => self.stage_formula_text(sheet, row, col, text.to_string()),
10335            None => {
10336                self.clear_staged_formula_text(sheet, row, col);
10337            }
10338        }
10339    }
10340
10341    pub fn set_row_hidden(
10342        &mut self,
10343        sheet: &str,
10344        row_1based: u32,
10345        hidden: bool,
10346        source: RowVisibilitySource,
10347    ) -> Result<(), crate::engine::EditorError> {
10348        self.observe_function_semantic_epoch()
10349            .map_err(crate::engine::EditorError::Excel)?;
10350        self.observe_function_semantic_epoch()
10351            .map_err(crate::engine::EditorError::Excel)?;
10352        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10353        let row0 = Self::normalize_row_1based(row_1based)?;
10354        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
10355            self.record_structural_change(StructuralScope::Region(Region::whole_row(
10356                sheet_id, row0,
10357            )));
10358            self.mark_data_edited();
10359        }
10360        Ok(())
10361    }
10362
10363    pub fn set_rows_hidden(
10364        &mut self,
10365        sheet: &str,
10366        start_row_1based: u32,
10367        end_row_1based: u32,
10368        hidden: bool,
10369        source: RowVisibilitySource,
10370    ) -> Result<(), crate::engine::EditorError> {
10371        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10372        let (start_row0, end_row0) =
10373            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
10374        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
10375            if start_row0 == end_row0 {
10376                self.record_structural_change(StructuralScope::Region(Region::whole_row(
10377                    sheet_id, start_row0,
10378                )));
10379            } else {
10380                self.record_structural_change(StructuralScope::Sheet(sheet_id));
10381            }
10382            self.mark_data_edited();
10383        }
10384        Ok(())
10385    }
10386
10387    pub fn is_row_hidden(
10388        &self,
10389        sheet: &str,
10390        row_1based: u32,
10391        source: Option<RowVisibilitySource>,
10392    ) -> Option<bool> {
10393        let sheet_id = self.graph.sheet_id(sheet)?;
10394        let row0 = row_1based.checked_sub(1)?;
10395        Some(
10396            self.row_visibility
10397                .get(&sheet_id)
10398                .map(|state| state.is_row_hidden(row0, source))
10399                .unwrap_or(false),
10400        )
10401    }
10402
10403    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
10404        let sheet_id = self.graph.sheet_id(sheet)?;
10405        Some(
10406            self.row_visibility
10407                .get(&sheet_id)
10408                .map(|state| state.version())
10409                .unwrap_or(0),
10410        )
10411    }
10412
10413    fn build_row_visibility_mask_for_view(
10414        &self,
10415        view: &RangeView<'_>,
10416        mode: VisibilityMaskMode,
10417    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
10418        let sheet_rows = view.sheet().nrows as usize;
10419        if sheet_rows == 0 || view.start_row() >= sheet_rows {
10420            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
10421        }
10422
10423        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
10424        let start_row0 = view.start_row() as u32;
10425        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
10426        let version = self
10427            .row_visibility
10428            .get(&sheet_id)
10429            .map(|state| state.version())
10430            .unwrap_or(0);
10431        let key = VisibilityMaskCacheKey {
10432            sheet_id,
10433            start_row0,
10434            end_row0,
10435            mode,
10436            version,
10437        };
10438
10439        if let Ok(cache) = self.row_visibility_mask_cache.read()
10440            && let Some(mask) = cache.get(&key)
10441        {
10442            #[cfg(test)]
10443            visibility_mask_test_hooks::inc_hit();
10444            return Some(mask.clone());
10445        }
10446
10447        #[cfg(test)]
10448        visibility_mask_test_hooks::inc_miss();
10449
10450        let state = self.row_visibility.get(&sheet_id);
10451        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
10452        for row0 in start_row0..=end_row0 {
10453            let manual_hidden = state
10454                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
10455                .unwrap_or(false);
10456            let filter_hidden = state
10457                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
10458                .unwrap_or(false);
10459
10460            let include = match mode {
10461                VisibilityMaskMode::IncludeAll => true,
10462                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
10463                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
10464                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
10465                    !(manual_hidden || filter_hidden)
10466                }
10467            };
10468            out.push(include);
10469        }
10470
10471        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
10472        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10473            const MAX_CACHE_ENTRIES: usize = 4096;
10474            if cache.len() >= MAX_CACHE_ENTRIES {
10475                cache.clear();
10476                #[cfg(test)]
10477                visibility_mask_test_hooks::inc_eviction();
10478            }
10479            cache.insert(key, mask.clone());
10480        }
10481
10482        Some(mask)
10483    }
10484
10485    fn observe_function_semantic_epoch(&mut self) -> Result<bool, ExcelError> {
10486        let changes =
10487            crate::function_registry::semantic_changes_since(self.function_semantic_epoch_seen);
10488        let global_changed = changes.epoch != self.function_semantic_epoch_seen;
10489        let provider_revision = self.resolver.planning_semantic_revision();
10490        let provider_changed = provider_revision != self.function_provider_revision_seen;
10491        if !global_changed && !provider_changed {
10492            return Ok(false);
10493        }
10494
10495        let changed = !changes.keys.is_empty();
10496        if global_changed && changed || provider_changed {
10497            self.cached_static_schedule = None;
10498            self.recent_schedules.clear();
10499            self.base_schedule = None;
10500        }
10501        self.function_semantic_epoch_seen = changes.epoch;
10502        self.function_provider_revision_seen = provider_revision;
10503        Ok(false)
10504    }
10505
10506    pub(crate) fn ast_uses_changed_function(
10507        ast: &ASTNode,
10508        changed: &BTreeSet<(String, String)>,
10509    ) -> bool {
10510        match &ast.node_type {
10511            ASTNodeType::Function { name, args } => {
10512                let normalized = name.to_uppercase();
10513                let mut spellings = vec![(String::new(), normalized.clone())];
10514                let mut stripped = normalized.as_str();
10515                loop {
10516                    let Some(rest) = ["_XLFN.", "_XLL.", "_XLWS."]
10517                        .iter()
10518                        .find_map(|prefix| stripped.strip_prefix(prefix))
10519                    else {
10520                        break;
10521                    };
10522                    stripped = rest;
10523                    spellings.push((String::new(), stripped.to_string()));
10524                }
10525                let resolved = crate::function_registry::resolve("", name);
10526                let directly_changed = spellings.iter().any(|spelling| changed.contains(spelling))
10527                    || resolved.as_ref().is_some_and(|resolved| {
10528                        changed.contains(&(
10529                            resolved.namespace.clone(),
10530                            resolved.canonical_name.clone(),
10531                        ))
10532                    });
10533                directly_changed
10534                    || resolved.is_none()
10535                    || args
10536                        .iter()
10537                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10538            }
10539            ASTNodeType::Call { callee, args } => {
10540                Self::ast_uses_changed_function(callee, changed)
10541                    || args
10542                        .iter()
10543                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10544            }
10545            ASTNodeType::UnaryOp { expr, .. } => Self::ast_uses_changed_function(expr, changed),
10546            ASTNodeType::BinaryOp { left, right, .. } => {
10547                Self::ast_uses_changed_function(left, changed)
10548                    || Self::ast_uses_changed_function(right, changed)
10549            }
10550            ASTNodeType::Array(rows) => rows
10551                .iter()
10552                .flatten()
10553                .any(|node| Self::ast_uses_changed_function(node, changed)),
10554            _ => false,
10555        }
10556    }
10557
10558    fn ast_contains_function(ast: &ASTNode) -> bool {
10559        match &ast.node_type {
10560            ASTNodeType::Function { .. } => true,
10561            ASTNodeType::Call { callee, args } => {
10562                Self::ast_contains_function(callee) || args.iter().any(Self::ast_contains_function)
10563            }
10564            ASTNodeType::UnaryOp { expr, .. } => Self::ast_contains_function(expr),
10565            ASTNodeType::BinaryOp { left, right, .. } => {
10566                Self::ast_contains_function(left) || Self::ast_contains_function(right)
10567            }
10568            ASTNodeType::Array(rows) => rows.iter().flatten().any(Self::ast_contains_function),
10569            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => false,
10570        }
10571    }
10572
10573    fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
10574        Region::rows_from(sheet_id, start_row0)
10575    }
10576
10577    fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
10578        Region::cols_from(sheet_id, start_col0)
10579    }
10580
10581    #[cfg(test)]
10582    pub(crate) fn force_non_cycle_schedule_fallback_for_test(&mut self) {
10583        self.force_non_cycle_schedule_fallback_for_test = true;
10584    }
10585
10586    fn materialize_deferred_sheet_before_structural_edit(
10587        &mut self,
10588        sheet: &str,
10589    ) -> Result<(), crate::engine::EditorError> {
10590        if self.staged_formulas.contains_key(sheet) {
10591            self.build_graph_for_sheets([sheet])?;
10592        }
10593        Ok(())
10594    }
10595
10596    fn structural_row_occupancy(
10597        &self,
10598        sheet: &str,
10599        sheet_id: SheetId,
10600    ) -> crate::engine::graph::StructuralOccupancy {
10601        if !self.graph.has_compressed_range_readers() {
10602            return crate::engine::graph::StructuralOccupancy::default();
10603        }
10604        let mut occupancy = self.graph.structural_occupancy(sheet_id);
10605        if let Some(arrow_sheet) = self.arrow_sheets.sheet(sheet) {
10606            occupancy.include_arrow_sheet(arrow_sheet);
10607            occupancy
10608        } else {
10609            // Missing Arrow state cannot prove an apparently empty column empty.
10610            crate::engine::graph::StructuralOccupancy::conservative()
10611        }
10612    }
10613
10614    fn structural_column_occupancy(&self) -> crate::engine::graph::StructuralOccupancy {
10615        // Arrow exposes occupied columns through chunk metadata and overlay maps,
10616        // but has no cheap occupied-row index. Column edits therefore deliberately
10617        // retain conservative cross-axis invalidation instead of scanning cells.
10618        crate::engine::graph::StructuralOccupancy::conservative()
10619    }
10620
10621    /// Insert rows (1-based) and mirror into Arrow store when enabled
10622    pub fn insert_rows(
10623        &mut self,
10624        sheet: &str,
10625        before: u32,
10626        count: u32,
10627    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10628    {
10629        if count == 0 {
10630            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10631        }
10632        self.observe_function_semantic_epoch()
10633            .map_err(crate::engine::EditorError::Excel)?;
10634        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10635        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10636        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10637        let before0 = before.saturating_sub(1);
10638        let affected_region = Self::structural_row_region(sheet_id, before0);
10639        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10640        let summary = {
10641            let mut editor =
10642                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10643            editor.insert_rows(sheet_id, before0, count)?
10644        };
10645        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10646            let before0 = before0 as usize;
10647            asheet.insert_rows(before0, count as usize);
10648        }
10649        self.purge_derived_formats_after_row(sheet_id, before0);
10650        self.mark_moved_formula_vertices_dirty(&summary);
10651        self.clear_computed_overlay_after_row(sheet, before0 as usize);
10652        self.shift_row_visibility_insert(sheet_id, before0, count);
10653        self.record_structural_change(StructuralScope::Region(affected_region));
10654        self.mark_topology_edited();
10655        Ok(summary)
10656    }
10657
10658    /// Delete rows (1-based) and mirror into Arrow store when enabled
10659    pub fn delete_rows(
10660        &mut self,
10661        sheet: &str,
10662        start: u32,
10663        count: u32,
10664    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10665    {
10666        if count == 0 {
10667            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10668        }
10669        self.observe_function_semantic_epoch()
10670            .map_err(crate::engine::EditorError::Excel)?;
10671        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10672        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10673        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10674        let start0 = start.saturating_sub(1);
10675        let affected_region = Self::structural_row_region(sheet_id, start0);
10676        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10677        let summary = {
10678            let mut editor =
10679                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10680            editor.delete_rows(sheet_id, start0, count)?
10681        };
10682        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10683            let start0 = start0 as usize;
10684            asheet.delete_rows(start0, count as usize);
10685        }
10686        self.purge_derived_formats_after_row(sheet_id, start0);
10687        self.mark_moved_formula_vertices_dirty(&summary);
10688        self.clear_computed_overlay_after_row(sheet, start0 as usize);
10689        self.shift_row_visibility_delete(sheet_id, start0, count);
10690        self.record_structural_change(StructuralScope::Region(affected_region));
10691        self.mark_topology_edited();
10692        Ok(summary)
10693    }
10694
10695    /// Insert columns (1-based) and mirror into Arrow store when enabled
10696    pub fn insert_columns(
10697        &mut self,
10698        sheet: &str,
10699        before: u32,
10700        count: u32,
10701    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10702    {
10703        if count == 0 {
10704            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10705        }
10706        self.observe_function_semantic_epoch()
10707            .map_err(crate::engine::EditorError::Excel)?;
10708        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10709        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10710        let sheet_id = self.graph.sheet_id(sheet).ok_or(
10711            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10712                name: sheet.to_string(),
10713                reason: "Unknown sheet".to_string(),
10714            },
10715        )?;
10716        let before0 = before.saturating_sub(1);
10717        let affected_region = Self::structural_col_region(sheet_id, before0);
10718        let occupancy = self.structural_column_occupancy();
10719        let summary = {
10720            let mut editor =
10721                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10722            editor.insert_columns(sheet_id, before0, count)?
10723        };
10724        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10725            let before0 = before0 as usize;
10726            asheet.insert_columns(before0, count as usize);
10727        }
10728        self.purge_derived_formats_after_col(sheet_id, before0);
10729        self.mark_moved_formula_vertices_dirty(&summary);
10730        self.clear_computed_overlay_after_col(sheet, before0 as usize);
10731        self.record_structural_change(StructuralScope::Region(affected_region));
10732        self.mark_topology_edited();
10733        Ok(summary)
10734    }
10735
10736    /// Delete columns (1-based) and mirror into Arrow store when enabled
10737    pub fn delete_columns(
10738        &mut self,
10739        sheet: &str,
10740        start: u32,
10741        count: u32,
10742    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10743    {
10744        if count == 0 {
10745            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10746        }
10747        self.observe_function_semantic_epoch()
10748            .map_err(crate::engine::EditorError::Excel)?;
10749        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10750        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10751        let sheet_id = self.graph.sheet_id(sheet).ok_or(
10752            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10753                name: sheet.to_string(),
10754                reason: "Unknown sheet".to_string(),
10755            },
10756        )?;
10757        let start0 = start.saturating_sub(1);
10758        let affected_region = Self::structural_col_region(sheet_id, start0);
10759        let occupancy = self.structural_column_occupancy();
10760        let summary = {
10761            let mut editor =
10762                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10763            editor.delete_columns(sheet_id, start0, count)?
10764        };
10765        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10766            let start0 = start0 as usize;
10767            asheet.delete_columns(start0, count as usize);
10768        }
10769        self.purge_derived_formats_after_col(sheet_id, start0);
10770        self.mark_moved_formula_vertices_dirty(&summary);
10771        self.clear_computed_overlay_after_col(sheet, start0 as usize);
10772        self.record_structural_change(StructuralScope::Region(affected_region));
10773        self.mark_topology_edited();
10774        Ok(summary)
10775    }
10776    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
10777    fn arrow_used_row_bounds(
10778        &self,
10779        sheet: &str,
10780        start_col: u32,
10781        end_col: u32,
10782    ) -> Option<(u32, u32)> {
10783        let a = self.sheet_store().sheet(sheet)?;
10784        if a.columns.is_empty() {
10785            return None;
10786        }
10787        let sc0 = start_col.saturating_sub(1) as usize;
10788        let ec0 = end_col.saturating_sub(1) as usize;
10789        let col_hi = a.columns.len().saturating_sub(1);
10790        if sc0 > col_hi {
10791            return None;
10792        }
10793        let ec0 = ec0.min(col_hi);
10794        // Pass-scoped cache with snapshot guard
10795        let snap = self.data_snapshot_id();
10796        let mut min_r0: Option<usize> = None;
10797        for ci in sc0..=ec0 {
10798            let sheet_id = self.graph.sheet_id(sheet)?;
10799            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
10800                g.as_ref()
10801                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
10802            }) {
10803                let mv = mv as usize;
10804                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
10805                continue;
10806            }
10807            // Compute and store
10808            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
10809            if let Ok(mut g) = self.row_bounds_cache.write() {
10810                g.get_or_insert_with(|| RowBoundsCache::new(snap))
10811                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
10812            }
10813            if let Some(m) = min_c {
10814                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
10815            }
10816        }
10817        min_r0?;
10818        let mut max_r0: Option<usize> = None;
10819        for ci in sc0..=ec0 {
10820            let sheet_id = self.graph.sheet_id(sheet)?;
10821            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
10822                g.as_ref()
10823                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
10824            }) {
10825                let mv = mv as usize;
10826                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
10827                continue;
10828            }
10829            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
10830            if let Ok(mut g) = self.row_bounds_cache.write() {
10831                g.get_or_insert_with(|| RowBoundsCache::new(snap))
10832                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
10833            }
10834            if let Some(m) = max_c {
10835                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
10836            }
10837        }
10838        match (min_r0, max_r0) {
10839            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
10840            _ => None,
10841        }
10842    }
10843
10844    fn scan_column_used_bounds(
10845        a: &crate::arrow_store::ArrowSheet,
10846        ci: usize,
10847    ) -> (Option<u32>, Option<u32>) {
10848        let col = &a.columns[ci];
10849
10850        // Min: scan dense chunks first, then sparse chunks in ascending index order.
10851        let mut min_r0: Option<u32> = None;
10852        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
10853            let tags = chunk.type_tag.values();
10854            for (off, &t) in tags.iter().enumerate() {
10855                let overlay_non_empty = chunk
10856                    .overlay
10857                    .get(off)
10858                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10859                    .unwrap_or(false)
10860                    || chunk
10861                        .computed_overlay
10862                        .get(off)
10863                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10864                        .unwrap_or(false);
10865                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10866                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10867                        break;
10868                    };
10869                    let row0 = chunk_start + off;
10870                    min_r0 = Some(row0 as u32);
10871                    break;
10872                }
10873            }
10874            if min_r0.is_some() {
10875                break;
10876            }
10877        }
10878        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
10879            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
10880            sparse_idxs.sort_unstable();
10881            for chunk_idx in sparse_idxs {
10882                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
10883                    continue;
10884                };
10885                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10886                    continue;
10887                };
10888                let tags = chunk.type_tag.values();
10889                for (off, &t) in tags.iter().enumerate() {
10890                    let overlay_non_empty = chunk
10891                        .overlay
10892                        .get(off)
10893                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10894                        .unwrap_or(false)
10895                        || chunk
10896                            .computed_overlay
10897                            .get(off)
10898                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10899                            .unwrap_or(false);
10900                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10901                        let row0 = chunk_start + off;
10902                        min_r0 = Some(row0 as u32);
10903                        break;
10904                    }
10905                }
10906                if min_r0.is_some() {
10907                    break;
10908                }
10909            }
10910        }
10911
10912        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
10913        let mut max_r0: Option<u32> = None;
10914        if !col.sparse_chunks.is_empty() {
10915            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
10916            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
10917            for chunk_idx in sparse_idxs {
10918                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
10919                    continue;
10920                };
10921                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10922                    continue;
10923                };
10924                let tags = chunk.type_tag.values();
10925                for (rev_idx, &t) in tags.iter().enumerate().rev() {
10926                    let overlay_non_empty = chunk
10927                        .overlay
10928                        .get(rev_idx)
10929                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10930                        .unwrap_or(false)
10931                        || chunk
10932                            .computed_overlay
10933                            .get(rev_idx)
10934                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10935                            .unwrap_or(false);
10936                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10937                        let row0 = chunk_start + rev_idx;
10938                        max_r0 = Some(row0 as u32);
10939                        break;
10940                    }
10941                }
10942                if max_r0.is_some() {
10943                    break;
10944                }
10945            }
10946        }
10947        if max_r0.is_none() {
10948            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
10949                let tags = chunk.type_tag.values();
10950                for (rev_idx, &t) in tags.iter().enumerate().rev() {
10951                    let overlay_non_empty = chunk
10952                        .overlay
10953                        .get(rev_idx)
10954                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10955                        .unwrap_or(false)
10956                        || chunk
10957                            .computed_overlay
10958                            .get(rev_idx)
10959                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10960                            .unwrap_or(false);
10961                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10962                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10963                            break;
10964                        };
10965                        let row0 = chunk_start + rev_idx;
10966                        max_r0 = Some(row0 as u32);
10967                        break;
10968                    }
10969                }
10970                if max_r0.is_some() {
10971                    break;
10972                }
10973            }
10974        }
10975
10976        (min_r0, max_r0)
10977    }
10978
10979    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
10980    fn arrow_used_col_bounds(
10981        &self,
10982        sheet: &str,
10983        start_row: u32,
10984        end_row: u32,
10985    ) -> Option<(u32, u32)> {
10986        let a = self.sheet_store().sheet(sheet)?;
10987        if a.columns.is_empty() {
10988            return None;
10989        }
10990        let sr0 = start_row.saturating_sub(1) as usize;
10991        let er0 = end_row.saturating_sub(1) as usize;
10992        if sr0 > er0 {
10993            return None;
10994        }
10995        // Map start/end rows into chunk ranges
10996        // We will scan each column for any non-empty within [sr0..=er0]
10997        let mut min_c0: Option<usize> = None;
10998        let mut max_c0: Option<usize> = None;
10999        // Precompute chunk bounds for row range
11000        for (ci, col) in a.columns.iter().enumerate() {
11001            let mut any_in_range = false;
11002
11003            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
11004                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11005                    return false;
11006                };
11007                let chunk_len = chunk.type_tag.len();
11008                if chunk_len == 0 {
11009                    return false;
11010                }
11011                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
11012                // check intersection
11013                if sr0 > chunk_end || er0 < chunk_start {
11014                    return false;
11015                }
11016                let start_off = sr0.max(chunk_start) - chunk_start;
11017                let end_off = er0.min(chunk_end) - chunk_start;
11018                let tags = chunk.type_tag.values();
11019                for off in start_off..=end_off {
11020                    let overlay_non_empty = chunk
11021                        .overlay
11022                        .get(off)
11023                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11024                        .unwrap_or(false)
11025                        || chunk
11026                            .computed_overlay
11027                            .get(off)
11028                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11029                            .unwrap_or(false);
11030                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
11031                        return true;
11032                    }
11033                }
11034                false
11035            };
11036
11037            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
11038                if scan_chunk(chunk_idx, chunk) {
11039                    any_in_range = true;
11040                    break;
11041                }
11042            }
11043
11044            if !any_in_range && !col.sparse_chunks.is_empty() {
11045                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
11046                    if scan_chunk(chunk_idx, chunk) {
11047                        any_in_range = true;
11048                        break;
11049                    }
11050                }
11051            }
11052
11053            if any_in_range {
11054                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
11055                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
11056            }
11057        }
11058        match (min_c0, max_c0) {
11059            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
11060            _ => None,
11061        }
11062    }
11063
11064    fn formula_row_bounds_for_columns(
11065        &self,
11066        sheet: &str,
11067        start_col: u32,
11068        end_col: u32,
11069    ) -> Option<(u32, u32)> {
11070        let sheet_id = self.graph.sheet_id(sheet)?;
11071        let sc0 = start_col.saturating_sub(1);
11072        let ec0 = end_col.saturating_sub(1);
11073        let mut min_r0: Option<u32> = None;
11074        let mut max_r0: Option<u32> = None;
11075
11076        if self.graph.sheet_index(sheet_id).is_some() {
11077            for vid in self.graph.vertices_in_cols(sheet_id, sc0, ec0) {
11078                if !matches!(
11079                    self.graph.get_vertex_kind(vid),
11080                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11081                ) {
11082                    continue;
11083                }
11084                let Some(row0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.row()) else {
11085                    continue;
11086                };
11087                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11088                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11089            }
11090        } else {
11091            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11092                if !matches!(
11093                    self.graph.get_vertex_kind(vid),
11094                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11095                ) {
11096                    continue;
11097                }
11098                let col0 = coord.col();
11099                if col0 < sc0 || col0 > ec0 {
11100                    continue;
11101                }
11102                let row0 = coord.row();
11103                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11104                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11105            }
11106        }
11107
11108        match (min_r0, max_r0) {
11109            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11110            _ => None,
11111        }
11112    }
11113
11114    fn formula_col_bounds_for_rows(
11115        &self,
11116        sheet: &str,
11117        start_row: u32,
11118        end_row: u32,
11119    ) -> Option<(u32, u32)> {
11120        let sheet_id = self.graph.sheet_id(sheet)?;
11121        let sr0 = start_row.saturating_sub(1);
11122        let er0 = end_row.saturating_sub(1);
11123        let mut min_c0: Option<u32> = None;
11124        let mut max_c0: Option<u32> = None;
11125
11126        if self.graph.sheet_index(sheet_id).is_some() {
11127            for vid in self.graph.vertices_in_rows(sheet_id, sr0, er0) {
11128                if !matches!(
11129                    self.graph.get_vertex_kind(vid),
11130                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11131                ) {
11132                    continue;
11133                }
11134                let Some(col0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.col()) else {
11135                    continue;
11136                };
11137                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11138                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11139            }
11140        } else {
11141            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11142                if !matches!(
11143                    self.graph.get_vertex_kind(vid),
11144                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11145                ) {
11146                    continue;
11147                }
11148                let row0 = coord.row();
11149                if row0 < sr0 || row0 > er0 {
11150                    continue;
11151                }
11152                let col0 = coord.col();
11153                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11154                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11155            }
11156        }
11157
11158        match (min_c0, max_c0) {
11159            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11160            _ => None,
11161        }
11162    }
11163
11164    fn union_used_bounds(
11165        first: Option<(u32, u32)>,
11166        second: Option<(u32, u32)>,
11167    ) -> Option<(u32, u32)> {
11168        match (first, second) {
11169            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
11170            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
11171            (None, None) => None,
11172        }
11173    }
11174
11175    /// Mirror a single cell value into the Arrow overlay if enabled.
11176    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
11177    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
11178        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11179            return;
11180        }
11181        if self.arrow_sheets.sheet(sheet).is_none() {
11182            self.arrow_sheets
11183                .sheets
11184                .push(crate::arrow_store::ArrowSheet {
11185                    name: std::sync::Arc::<str>::from(sheet),
11186                    date_system: self.config.date_system,
11187                    columns: Vec::new(),
11188                    nrows: 0,
11189                    chunk_starts: Vec::new(),
11190                    chunk_rows: 32 * 1024,
11191                });
11192        }
11193
11194        let row0 = row.saturating_sub(1) as usize;
11195        let col0 = col.saturating_sub(1) as usize;
11196
11197        let asheet = self
11198            .arrow_sheets
11199            .sheet_mut(sheet)
11200            .expect("ArrowSheet must exist");
11201
11202        let cur_cols = asheet.columns.len();
11203        if col0 >= cur_cols {
11204            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11205        }
11206
11207        if row0 >= asheet.nrows as usize {
11208            if asheet.columns.is_empty() {
11209                asheet.insert_columns(0, 1);
11210            }
11211            asheet.ensure_row_capacity(row0 + 1);
11212        }
11213        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
11214            let ov =
11215                crate::arrow_store::OverlayValue::from_literal_value(value, asheet.date_system);
11216            let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
11217                let _ = ch.overlay.set(in_off, ov);
11218                let format = match value {
11219                    LiteralValue::Date(_) => Some(crate::format::FormatId::DATE),
11220                    LiteralValue::DateTime(_) => Some(crate::format::FormatId::DATETIME),
11221                    LiteralValue::Time(_) => Some(crate::format::FormatId::TIME),
11222                    LiteralValue::Duration(_) => Some(crate::format::FormatId::DURATION),
11223                    _ => None,
11224                };
11225                ch.overlay.set_format(in_off, format);
11226                // A user edit must invalidate any computed (formula/spill) overlay entry at
11227                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
11228                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
11229                // the edited base value under the read cascade.
11230                ch.computed_overlay.remove(in_off)
11231            } else {
11232                return;
11233            };
11234            // Heuristic compaction: > len/50 or > 1024
11235            let abs_threshold = 1024usize;
11236            let frac_den = 50usize;
11237            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
11238            if freed > 0 {
11239                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
11240            }
11241            self.adjust_computed_overlay_bytes(computed_delta);
11242        }
11243    }
11244
11245    /// Remove a delta-overlay entry for a single cell (if present).
11246    ///
11247    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
11248    /// does not continue to mask computed overlay outputs.
11249    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
11250        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11251            return;
11252        }
11253        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11254            return;
11255        };
11256        let row0 = row.saturating_sub(1) as usize;
11257        let col0 = col.saturating_sub(1) as usize;
11258        if row0 >= asheet.nrows as usize {
11259            return;
11260        }
11261        if col0 >= asheet.columns.len() {
11262            return;
11263        }
11264        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11265            return;
11266        };
11267        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
11268            let _ = ch.overlay.remove(in_off);
11269        }
11270    }
11271
11272    fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
11273        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11274            return;
11275        }
11276
11277        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11278            return;
11279        };
11280        if start_row0 >= asheet.nrows as usize {
11281            return;
11282        }
11283
11284        let starts = asheet.chunk_starts.clone();
11285        let nrows = asheet.nrows as usize;
11286        let mut delta = 0isize;
11287        for col in &mut asheet.columns {
11288            for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
11289                let Some(&chunk_start) = starts.get(chunk_idx) else {
11290                    continue;
11291                };
11292                let chunk_end = starts
11293                    .get(chunk_idx + 1)
11294                    .copied()
11295                    .unwrap_or(nrows)
11296                    .min(chunk_start.saturating_add(ch.len()));
11297                if chunk_end <= start_row0 {
11298                    continue;
11299                }
11300                if chunk_start >= start_row0 {
11301                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11302                } else {
11303                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11304                    delta = delta
11305                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11306                }
11307            }
11308
11309            for (chunk_idx, ch) in &mut col.sparse_chunks {
11310                let Some(&chunk_start) = starts.get(*chunk_idx) else {
11311                    continue;
11312                };
11313                let chunk_end = starts
11314                    .get(*chunk_idx + 1)
11315                    .copied()
11316                    .unwrap_or(nrows)
11317                    .min(chunk_start.saturating_add(ch.len()));
11318                if chunk_end <= start_row0 {
11319                    continue;
11320                }
11321                if chunk_start >= start_row0 {
11322                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11323                } else {
11324                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11325                    delta = delta
11326                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11327                }
11328            }
11329        }
11330        self.adjust_computed_overlay_bytes(delta);
11331    }
11332
11333    fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
11334        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11335            return;
11336        }
11337
11338        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11339            return;
11340        };
11341        if start_col0 >= asheet.columns.len() {
11342            return;
11343        }
11344
11345        let mut delta = 0isize;
11346        for col in asheet.columns.iter_mut().skip(start_col0) {
11347            for ch in &mut col.chunks {
11348                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11349            }
11350            for ch in col.sparse_chunks.values_mut() {
11351                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11352            }
11353        }
11354        self.adjust_computed_overlay_bytes(delta);
11355    }
11356
11357    #[inline]
11358    fn literal_to_overlay_value(
11359        value: &LiteralValue,
11360        date_system: crate::engine::DateSystem,
11361    ) -> crate::arrow_store::OverlayValue {
11362        crate::arrow_store::OverlayValue::from_literal_value(value, date_system)
11363    }
11364
11365    fn arrow_sheet_date_system(&self, sheet: &str) -> crate::engine::DateSystem {
11366        self.arrow_sheets
11367            .sheet(sheet)
11368            .map(|sheet| sheet.date_system)
11369            .unwrap_or(self.config.date_system)
11370    }
11371
11372    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
11373    /// absent and explicit `Empty`.
11374    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11375        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11376            return None;
11377        }
11378        let asheet = self.arrow_sheets.sheet(sheet)?;
11379        let row0 = row.saturating_sub(1) as usize;
11380        let col0 = col.saturating_sub(1) as usize;
11381        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11382            return None;
11383        }
11384        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11385        let ch = asheet.columns[col0].chunk(ch_idx)?;
11386        ch.overlay
11387            .get_scalar(in_off)
11388            .map(|ov| ov.to_literal_for(asheet.date_system))
11389    }
11390
11391    /// Read a single cell's computed overlay entry (if present), preserving the distinction
11392    /// between absent and explicit `Empty`.
11393    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11394        if !(self.config.arrow_storage_enabled
11395            && self.config.delta_overlay_enabled
11396            && self.config.write_formula_overlay_enabled)
11397        {
11398            return None;
11399        }
11400        let asheet = self.arrow_sheets.sheet(sheet)?;
11401        let row0 = row.saturating_sub(1) as usize;
11402        let col0 = col.saturating_sub(1) as usize;
11403        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11404            return None;
11405        }
11406        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11407        let ch = asheet.columns[col0].chunk(ch_idx)?;
11408        ch.computed_overlay
11409            .get_scalar(in_off)
11410            .map(|ov| ov.to_literal_for(asheet.date_system))
11411    }
11412
11413    fn set_delta_overlay_cell_raw(
11414        &mut self,
11415        sheet: &str,
11416        row: u32,
11417        col: u32,
11418        value: Option<LiteralValue>,
11419    ) {
11420        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11421            return;
11422        }
11423
11424        self.ensure_arrow_sheet(sheet);
11425        let date_system = self.arrow_sheet_date_system(sheet);
11426        let ov_opt = value
11427            .as_ref()
11428            .map(|value| Self::literal_to_overlay_value(value, date_system));
11429        let row0 = row.saturating_sub(1) as usize;
11430        let col0 = col.saturating_sub(1) as usize;
11431        let asheet = self
11432            .arrow_sheets
11433            .sheet_mut(sheet)
11434            .expect("ArrowSheet must exist");
11435
11436        let cur_cols = asheet.columns.len();
11437        if col0 >= cur_cols {
11438            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11439        }
11440        if row0 >= asheet.nrows as usize {
11441            if asheet.columns.is_empty() {
11442                asheet.insert_columns(0, 1);
11443            }
11444            asheet.ensure_row_capacity(row0 + 1);
11445        }
11446
11447        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11448            return;
11449        };
11450        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11451            return;
11452        };
11453
11454        if let Some(ov) = ov_opt {
11455            let _ = ch.overlay.set(in_off, ov);
11456        } else {
11457            let _ = ch.overlay.remove(in_off);
11458        }
11459    }
11460
11461    fn set_computed_overlay_cell_raw(
11462        &mut self,
11463        sheet: &str,
11464        row: u32,
11465        col: u32,
11466        value: Option<LiteralValue>,
11467    ) {
11468        if !(self.config.arrow_storage_enabled
11469            && self.config.delta_overlay_enabled
11470            && self.config.write_formula_overlay_enabled)
11471        {
11472            return;
11473        }
11474
11475        self.ensure_arrow_sheet(sheet);
11476        let date_system = self.arrow_sheet_date_system(sheet);
11477        let ov_opt = value
11478            .as_ref()
11479            .map(|value| Self::literal_to_overlay_value(value, date_system));
11480        let row0 = row.saturating_sub(1) as usize;
11481        let col0 = col.saturating_sub(1) as usize;
11482        let asheet = self
11483            .arrow_sheets
11484            .sheet_mut(sheet)
11485            .expect("ArrowSheet must exist");
11486
11487        let cur_cols = asheet.columns.len();
11488        if col0 >= cur_cols {
11489            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11490        }
11491        if row0 >= asheet.nrows as usize {
11492            if asheet.columns.is_empty() {
11493                asheet.insert_columns(0, 1);
11494            }
11495            asheet.ensure_row_capacity(row0 + 1);
11496        }
11497
11498        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11499            return;
11500        };
11501        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11502            return;
11503        };
11504
11505        let delta = if let Some(ov) = ov_opt {
11506            ch.computed_overlay.set(in_off, ov)
11507        } else {
11508            ch.computed_overlay.remove(in_off)
11509        };
11510        self.adjust_computed_overlay_bytes(delta);
11511    }
11512
11513    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
11514        use crate::engine::ArrowOp;
11515
11516        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
11517            Box::new(batch.ops.iter().rev())
11518        } else {
11519            Box::new(batch.ops.iter())
11520        };
11521
11522        for op in iter {
11523            match op {
11524                ArrowOp::SetDeltaCell {
11525                    sheet_id,
11526                    row0,
11527                    col0,
11528                    old,
11529                    new,
11530                } => {
11531                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11532                    let v = if undo { old.clone() } else { new.clone() };
11533                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11534                }
11535                ArrowOp::SetComputedCell {
11536                    sheet_id,
11537                    row0,
11538                    col0,
11539                    old,
11540                    new,
11541                } => {
11542                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11543                    let v = if undo { old.clone() } else { new.clone() };
11544                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11545                }
11546                ArrowOp::RestoreComputedRect {
11547                    sheet_id,
11548                    sr0,
11549                    sc0,
11550                    er0,
11551                    ec0,
11552                    old,
11553                    new,
11554                } => {
11555                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11556                    let vals = if undo { old } else { new };
11557                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
11558                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
11559                    for r in 0..height {
11560                        for c in 0..width {
11561                            let v = vals
11562                                .get(r)
11563                                .and_then(|row| row.get(c))
11564                                .cloned()
11565                                .unwrap_or(LiteralValue::Empty);
11566                            self.set_computed_overlay_cell_raw(
11567                                &sheet,
11568                                *sr0 + 1 + r as u32,
11569                                *sc0 + 1 + c as u32,
11570                                Some(v),
11571                            );
11572                        }
11573                    }
11574                }
11575                ArrowOp::InsertRows {
11576                    sheet_id,
11577                    before0,
11578                    count,
11579                } => {
11580                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11581                    self.ensure_arrow_sheet(&sheet);
11582                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11583                        if undo {
11584                            asheet.delete_rows(*before0 as usize, *count as usize);
11585                        } else {
11586                            asheet.insert_rows(*before0 as usize, *count as usize);
11587                        }
11588                    }
11589                    self.purge_derived_formats_after_row(*sheet_id, *before0);
11590                }
11591                ArrowOp::InsertCols {
11592                    sheet_id,
11593                    before0,
11594                    count,
11595                } => {
11596                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11597                    self.ensure_arrow_sheet(&sheet);
11598                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11599                        if undo {
11600                            asheet.delete_columns(*before0 as usize, *count as usize);
11601                        } else {
11602                            asheet.insert_columns(*before0 as usize, *count as usize);
11603                        }
11604                    }
11605                    self.purge_derived_formats_after_col(*sheet_id, *before0);
11606                }
11607            }
11608        }
11609    }
11610
11611    fn record_spill_ops_into_arrow_undo(
11612        &mut self,
11613        undo: &mut crate::engine::ArrowUndoBatch,
11614        events: &[crate::engine::ChangeEvent],
11615    ) {
11616        use crate::engine::ChangeEvent;
11617        use formualizer_common::LiteralValue;
11618
11619        #[allow(clippy::type_complexity)]
11620        let rect_from_snapshot =
11621            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
11622             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
11623                if snap.target_cells.is_empty() {
11624                    return None;
11625                }
11626                let sheet_id = snap.target_cells[0].sheet_id;
11627                let sr0 = snap.target_cells[0].coord.row();
11628                let sc0 = snap.target_cells[0].coord.col();
11629                if snap.values.is_empty() || snap.values[0].is_empty() {
11630                    return None;
11631                }
11632                let h = snap.values.len() as u32;
11633                let w = snap.values[0].len() as u32;
11634                let er0 = sr0.saturating_add(h.saturating_sub(1));
11635                let ec0 = sc0.saturating_add(w.saturating_sub(1));
11636                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
11637            };
11638
11639        for ev in events {
11640            match ev {
11641                ChangeEvent::SpillCommitted { old, new, .. } => {
11642                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
11643                        let old_vals = if let Some(old_snap) = old {
11644                            rect_from_snapshot(old_snap)
11645                                .map(|(_, _, _, _, _, v)| v)
11646                                .unwrap_or_else(|| {
11647                                    vec![
11648                                        vec![LiteralValue::Empty; new_vals[0].len()];
11649                                        new_vals.len()
11650                                    ]
11651                                })
11652                        } else {
11653                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
11654                        };
11655                        undo.record_restore_computed_rect(
11656                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11657                        );
11658                    }
11659                }
11660                ChangeEvent::SpillCleared { old, .. } => {
11661                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
11662                        let new_vals =
11663                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
11664                        undo.record_restore_computed_rect(
11665                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11666                        );
11667                    }
11668                }
11669                _ => {}
11670            }
11671        }
11672    }
11673
11674    /// Mirror a value into the computed overlay (formula/spill outputs).
11675    ///
11676    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
11677    /// If the cap is exceeded, computed overlays are compacted into base lanes.
11678    fn mirror_value_to_computed_overlay(
11679        &mut self,
11680        sheet: &str,
11681        row: u32,
11682        col: u32,
11683        value: &LiteralValue,
11684    ) {
11685        if !(self.config.arrow_storage_enabled
11686            && self.config.delta_overlay_enabled
11687            && self.config.write_formula_overlay_enabled)
11688        {
11689            return;
11690        }
11691        if self.computed_overlay_mirroring_disabled {
11692            return;
11693        }
11694
11695        let date_system = self.arrow_sheet_date_system(sheet);
11696        let ov = Self::literal_to_overlay_value(value, date_system);
11697        self.write_computed_overlay_value_0based(
11698            sheet,
11699            row.saturating_sub(1),
11700            col.saturating_sub(1),
11701            ov,
11702        );
11703    }
11704
11705    fn record_derived_format(&self, vertex_id: VertexId, format: Option<crate::format::FormatId>) {
11706        if let Some(cell) = self.graph.get_cell_ref(vertex_id) {
11707            self.record_derived_format_at(cell, format);
11708        }
11709    }
11710
11711    fn record_derived_format_at(&self, cell: CellRef, format: Option<crate::format::FormatId>) {
11712        #[cfg(test)]
11713        self.derived_format_operations_for_test
11714            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11715        let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
11716        self.derived_formats.set(cell, format);
11717    }
11718
11719    fn clear_cell_format_state(&mut self, sheet: &str, cell: CellRef) {
11720        self.derived_formats.set(cell, None);
11721        if let Some(arrow) = self.arrow_sheets.sheet_mut(sheet) {
11722            arrow.clear_format(cell.coord.row() as usize, cell.coord.col() as usize);
11723        }
11724    }
11725
11726    fn clear_logged_cell_format_states(&mut self, events: &[ChangeEvent]) {
11727        let cells = events
11728            .iter()
11729            .filter_map(|event| match event {
11730                ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
11731                    Some(*addr)
11732                }
11733                _ => None,
11734            })
11735            .collect::<FxHashSet<_>>();
11736        for cell in cells {
11737            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
11738            self.clear_cell_format_state(&sheet, cell);
11739        }
11740    }
11741
11742    fn purge_derived_formats_after_row(&mut self, sheet_id: SheetId, start0: u32) {
11743        self.derived_formats
11744            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.row() < start0);
11745    }
11746
11747    fn purge_derived_formats_after_col(&mut self, sheet_id: SheetId, start0: u32) {
11748        self.derived_formats
11749            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.col() < start0);
11750    }
11751
11752    fn purge_derived_formats_for_sheet(&mut self, sheet_id: SheetId) {
11753        self.derived_formats
11754            .retain(|cell| cell.sheet_id != sheet_id);
11755    }
11756
11757    #[cfg(test)]
11758    pub(crate) fn debug_computed_overlay_format_0based(
11759        &self,
11760        sheet: &str,
11761        row0: u32,
11762        col0: u32,
11763    ) -> Option<crate::format::FormatId> {
11764        let sheet = self.arrow_sheets.sheet(sheet)?;
11765        let (chunk_idx, row_in_chunk) = sheet.chunk_of_row(row0 as usize)?;
11766        sheet
11767            .columns
11768            .get(col0 as usize)?
11769            .chunk(chunk_idx)?
11770            .computed_overlay
11771            .get_format(row_in_chunk)
11772    }
11773
11774    #[cfg(test)]
11775    pub(crate) fn debug_computed_overlay_chunk_has_formats_0based(
11776        &self,
11777        sheet: &str,
11778        row0: u32,
11779        col0: u32,
11780    ) -> bool {
11781        let Some(sheet) = self.arrow_sheets.sheet(sheet) else {
11782            return false;
11783        };
11784        let Some((chunk_idx, _)) = sheet.chunk_of_row(row0 as usize) else {
11785            return false;
11786        };
11787        sheet
11788            .columns
11789            .get(col0 as usize)
11790            .and_then(|column| column.chunk(chunk_idx))
11791            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
11792    }
11793
11794    #[cfg(test)]
11795    pub(crate) fn debug_clear_derived_format_0based(&mut self, sheet: &str, row0: u32, col0: u32) {
11796        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
11797            self.derived_formats
11798                .set(CellRef::new_absolute(sheet_id, row0, col0), None);
11799        }
11800    }
11801
11802    #[cfg(test)]
11803    pub(crate) fn debug_record_derived_format_0based(
11804        &self,
11805        sheet: &str,
11806        row0: u32,
11807        col0: u32,
11808        format: Option<crate::format::FormatId>,
11809    ) {
11810        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
11811            self.record_derived_format_at(CellRef::new_absolute(sheet_id, row0, col0), format);
11812        }
11813    }
11814
11815    #[cfg(test)]
11816    pub(crate) fn debug_derived_format_0based(
11817        &self,
11818        sheet: &str,
11819        row0: u32,
11820        col0: u32,
11821    ) -> Option<crate::format::FormatId> {
11822        let sheet_id = self.graph.sheet_id(sheet)?;
11823        self.derived_formats
11824            .get(&CellRef::new_absolute(sheet_id, row0, col0))
11825    }
11826
11827    #[cfg(test)]
11828    pub(crate) fn debug_reset_format_write_operation_counts(&mut self) {
11829        self.derived_format_operations_for_test
11830            .store(0, std::sync::atomic::Ordering::Relaxed);
11831        self.computed_overlay_set_explicit_entry_operations_for_test = 0;
11832        self.computed_overlay_stale_clear_range_effects_for_test = 0;
11833        self.computed_overlay_stale_clear_offset_attempts_for_test = 0;
11834        self.computed_format_vector_allocations_for_test
11835            .store(0, std::sync::atomic::Ordering::Relaxed);
11836    }
11837
11838    #[cfg(test)]
11839    pub(crate) fn debug_format_write_operation_counts(&self) -> (u64, u64, u64, u64, u64) {
11840        (
11841            self.derived_format_operations_for_test
11842                .load(std::sync::atomic::Ordering::Relaxed),
11843            self.computed_overlay_set_explicit_entry_operations_for_test,
11844            self.computed_format_vector_allocations_for_test
11845                .load(std::sync::atomic::Ordering::Relaxed),
11846            self.computed_overlay_stale_clear_range_effects_for_test,
11847            self.computed_overlay_stale_clear_offset_attempts_for_test,
11848        )
11849    }
11850
11851    fn write_computed_overlay_format_0based(
11852        &mut self,
11853        sheet: &str,
11854        row0: u32,
11855        col0: u32,
11856        format: Option<crate::format::FormatId>,
11857    ) {
11858        self.ensure_arrow_sheet(sheet);
11859        let (row0, col0) = (row0 as usize, col0 as usize);
11860        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11861            return;
11862        };
11863        if col0 >= asheet.columns.len() {
11864            asheet.insert_columns(asheet.columns.len(), col0 + 1 - asheet.columns.len());
11865        }
11866        if row0 >= asheet.nrows as usize {
11867            asheet.ensure_row_capacity(row0 + 1);
11868        }
11869        let Some((chunk, offset)) = asheet.chunk_of_row(row0) else {
11870            return;
11871        };
11872        if let Some(chunk) = asheet.ensure_column_chunk_mut(col0, chunk) {
11873            chunk.computed_overlay.set_format(offset, format);
11874        }
11875    }
11876
11877    /// One unbuffered computed write of `value` and its derived `format` at
11878    /// `cell` (a single sheet lookup; the same writes as
11879    /// `write_computed_overlay_value_0based` then
11880    /// `write_computed_overlay_format_0based`).
11881    fn write_computed_cell_0based(
11882        &mut self,
11883        cell: CellRef,
11884        value: &LiteralValue,
11885        format: Option<crate::format::FormatId>,
11886    ) {
11887        if !(self.config.arrow_storage_enabled
11888            && self.config.delta_overlay_enabled
11889            && self.config.write_formula_overlay_enabled)
11890            || self.computed_overlay_mirroring_disabled
11891        {
11892            return;
11893        }
11894        let sheet = self.graph.sheet_name(cell.sheet_id);
11895        let index = match self
11896            .arrow_sheets
11897            .sheets
11898            .iter()
11899            .position(|s| s.name.as_ref() == sheet)
11900        {
11901            Some(index) => index,
11902            None => {
11903                let sheet = sheet.to_string();
11904                self.ensure_arrow_sheet(&sheet);
11905                self.arrow_sheets.sheets.len() - 1
11906            }
11907        };
11908        let (row0, col0) = (cell.coord.row() as usize, cell.coord.col() as usize);
11909        let asheet = &mut self.arrow_sheets.sheets[index];
11910        let ov = Self::literal_to_overlay_value(value, asheet.date_system);
11911        let cur_cols = asheet.columns.len();
11912        if col0 >= cur_cols {
11913            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11914        }
11915        if row0 >= asheet.nrows as usize {
11916            asheet.ensure_row_capacity(row0 + 1);
11917        }
11918        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11919            return;
11920        };
11921        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11922            return;
11923        };
11924        let delta = ch.computed_overlay.set_scalar(in_off, ov);
11925        ch.computed_overlay.set_format(in_off, format);
11926        self.adjust_computed_overlay_bytes(delta);
11927        if let Some(cap) = self.config.max_overlay_memory_bytes
11928            && self.computed_overlay_bytes_estimate > cap
11929        {
11930            self.disable_computed_overlay_mirroring_due_to_budget(cap);
11931        }
11932    }
11933
11934    fn write_computed_overlay_value_0based(
11935        &mut self,
11936        sheet: &str,
11937        row0: u32,
11938        col0: u32,
11939        value: OverlayValue,
11940    ) {
11941        if !(self.config.arrow_storage_enabled
11942            && self.config.delta_overlay_enabled
11943            && self.config.write_formula_overlay_enabled)
11944        {
11945            return;
11946        }
11947        if self.computed_overlay_mirroring_disabled {
11948            return;
11949        }
11950
11951        self.ensure_arrow_sheet(sheet);
11952
11953        let row0 = row0 as usize;
11954        let col0 = col0 as usize;
11955        let asheet = self
11956            .arrow_sheets
11957            .sheet_mut(sheet)
11958            .expect("ArrowSheet must exist");
11959
11960        let cur_cols = asheet.columns.len();
11961        if col0 >= cur_cols {
11962            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11963        }
11964
11965        if row0 >= asheet.nrows as usize {
11966            if asheet.columns.is_empty() {
11967                asheet.insert_columns(0, 1);
11968            }
11969            asheet.ensure_row_capacity(row0 + 1);
11970        }
11971
11972        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11973            return;
11974        };
11975        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11976            return;
11977        };
11978
11979        let delta = ch.computed_overlay.set_scalar(in_off, value);
11980        self.adjust_computed_overlay_bytes(delta);
11981
11982        if let Some(cap) = self.config.max_overlay_memory_bytes
11983            && self.computed_overlay_bytes_estimate > cap
11984        {
11985            self.disable_computed_overlay_mirroring_due_to_budget(cap);
11986        }
11987    }
11988
11989    pub(crate) fn plan_computed_write_coalescing(
11990        &self,
11991        buffer: &ComputedWriteBuffer,
11992    ) -> ComputedWriteCoalescingPlan {
11993        self.plan_computed_write_coalescing_from_writes(
11994            buffer.writes().iter().cloned(),
11995            buffer.formats_present,
11996        )
11997    }
11998
11999    fn plan_owned_computed_write_coalescing(
12000        &self,
12001        writes: Vec<ComputedWrite>,
12002        formats_present: bool,
12003    ) -> ComputedWriteCoalescingPlan {
12004        self.plan_computed_write_coalescing_from_writes(writes, formats_present)
12005    }
12006
12007    fn plan_computed_write_coalescing_from_writes(
12008        &self,
12009        writes: impl IntoIterator<Item = ComputedWrite>,
12010        formats_present: bool,
12011    ) -> ComputedWriteCoalescingPlan {
12012        let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
12013            BTreeMap::new();
12014        let mut input_cells = 0usize;
12015        // One sheet lookup per sheet run of writes, not per cell.
12016        let mut located: Option<(SheetId, Option<&crate::arrow_store::ArrowSheet>)> = None;
12017
12018        for write in writes {
12019            match write {
12020                ComputedWrite::Cell {
12021                    seq,
12022                    sheet_id,
12023                    row0,
12024                    col0,
12025                    value,
12026                    format_id,
12027                } => {
12028                    input_cells = input_cells.saturating_add(1);
12029                    let sheet = match located {
12030                        Some((id, sheet)) if id == sheet_id => sheet,
12031                        _ => {
12032                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12033                            located = Some((sheet_id, sheet));
12034                            sheet
12035                        }
12036                    };
12037                    let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12038                        Some(sheet) => {
12039                            Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize)
12040                        }
12041                        None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12042                            row0 as usize,
12043                            32 * 1024,
12044                        ),
12045                    };
12046                    groups
12047                        .entry(ComputedWriteChunkKey {
12048                            sheet_id,
12049                            col0,
12050                            chunk_idx,
12051                            chunk_start_row0,
12052                        })
12053                        .or_default()
12054                        .push(ComputedWriteChunkEntryPlan {
12055                            row_in_chunk,
12056                            seq,
12057                            value,
12058                            format_id,
12059                        });
12060                }
12061                ComputedWrite::Run {
12062                    seq,
12063                    sheet_id,
12064                    row0,
12065                    col0,
12066                    entries,
12067                } => {
12068                    input_cells = input_cells.saturating_add(entries.len());
12069                    let sheet = match located {
12070                        Some((id, sheet)) if id == sheet_id => sheet,
12071                        _ => {
12072                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12073                            located = Some((sheet_id, sheet));
12074                            sheet
12075                        }
12076                    };
12077                    // Rows are located one by one (a binary search, no sheet
12078                    // lookup); the group map is touched once per chunk
12079                    // segment of the run.
12080                    let mut segment: Vec<ComputedWriteChunkEntryPlan> = Vec::new();
12081                    let mut segment_key: Option<ComputedWriteChunkKey> = None;
12082                    for (k, (value, format_id)) in entries.into_iter().enumerate() {
12083                        let row = row0.saturating_add(k as u32) as usize;
12084                        let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12085                            Some(sheet) => {
12086                                Self::locate_row_in_sheet_for_computed_write_plan(sheet, row)
12087                            }
12088                            None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12089                                row,
12090                                32 * 1024,
12091                            ),
12092                        };
12093                        let key = ComputedWriteChunkKey {
12094                            sheet_id,
12095                            col0,
12096                            chunk_idx,
12097                            chunk_start_row0,
12098                        };
12099                        if segment_key != Some(key)
12100                            && let Some(done) = segment_key.replace(key)
12101                        {
12102                            groups.entry(done).or_default().append(&mut segment);
12103                        }
12104                        segment.push(ComputedWriteChunkEntryPlan {
12105                            row_in_chunk,
12106                            seq,
12107                            value,
12108                            format_id,
12109                        });
12110                    }
12111                    if let Some(done) = segment_key {
12112                        groups.entry(done).or_default().append(&mut segment);
12113                    }
12114                }
12115                ComputedWrite::Rect {
12116                    seq,
12117                    sheet_id,
12118                    sr0,
12119                    sc0,
12120                    values,
12121                } => {
12122                    for (r_off, row) in values.into_iter().enumerate() {
12123                        for (c_off, value) in row.into_iter().enumerate() {
12124                            input_cells = input_cells.saturating_add(1);
12125                            self.push_computed_write_plan_entry(
12126                                &mut groups,
12127                                seq,
12128                                sheet_id,
12129                                sr0.saturating_add(r_off as u32),
12130                                sc0.saturating_add(c_off as u32),
12131                                value,
12132                                None,
12133                            );
12134                        }
12135                    }
12136                }
12137            }
12138        }
12139
12140        let mut plan = ComputedWriteCoalescingPlan {
12141            chunks: Vec::with_capacity(groups.len()),
12142            input_cells,
12143            coalesced_cells: 0,
12144            overwritten_cells: 0,
12145        };
12146        for (key, entries) in groups {
12147            let computed_lane_has_formats =
12148                self.computed_overlay_chunk_has_formats(key.sheet_id, key.col0, key.chunk_idx);
12149            let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(
12150                key,
12151                entries,
12152                formats_present,
12153                computed_lane_has_formats,
12154            );
12155            #[cfg(test)]
12156            if matches!(
12157                &chunk_plan.format_effect,
12158                ComputedWriteChunkFormatEffect::SetExplicit(_)
12159            ) {
12160                self.computed_format_vector_allocations_for_test
12161                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12162            }
12163            plan.coalesced_cells = plan
12164                .coalesced_cells
12165                .saturating_add(chunk_plan.entries.len());
12166            plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
12167            plan.chunks.push(chunk_plan);
12168        }
12169        debug_assert_eq!(
12170            plan.input_cells,
12171            plan.coalesced_cells.saturating_add(plan.overwritten_cells)
12172        );
12173        plan
12174    }
12175
12176    fn push_computed_write_plan_entry(
12177        &self,
12178        groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
12179        seq: u64,
12180        sheet_id: SheetId,
12181        row0: u32,
12182        col0: u32,
12183        value: OverlayValue,
12184        format_id: Option<crate::format::FormatId>,
12185    ) {
12186        let (chunk_idx, chunk_start_row0, row_in_chunk) =
12187            self.locate_computed_write_chunk(sheet_id, row0);
12188        let key = ComputedWriteChunkKey {
12189            sheet_id,
12190            col0,
12191            chunk_idx,
12192            chunk_start_row0,
12193        };
12194        groups
12195            .entry(key)
12196            .or_default()
12197            .push(ComputedWriteChunkEntryPlan {
12198                row_in_chunk,
12199                seq,
12200                value,
12201                format_id,
12202            });
12203    }
12204
12205    fn computed_overlay_chunk_has_formats(
12206        &self,
12207        sheet_id: SheetId,
12208        col0: u32,
12209        chunk_idx: usize,
12210    ) -> bool {
12211        let sheet_name = self.graph.sheet_name(sheet_id);
12212        self.arrow_sheets
12213            .sheet(sheet_name)
12214            .and_then(|sheet| sheet.columns.get(col0 as usize))
12215            .and_then(|column| column.chunk(chunk_idx))
12216            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
12217    }
12218
12219    fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
12220        let sheet_name = self.graph.sheet_name(sheet_id);
12221        if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
12222            return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
12223        }
12224        Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
12225    }
12226
12227    fn locate_row_in_sheet_for_computed_write_plan(
12228        sheet: &crate::arrow_store::ArrowSheet,
12229        row0: usize,
12230    ) -> (usize, u32, usize) {
12231        if row0 < sheet.nrows as usize
12232            && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
12233        {
12234            let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
12235            return (chunk_idx, chunk_start as u32, row_in_chunk);
12236        }
12237
12238        let chunk_rows = sheet.chunk_rows.max(1);
12239        if sheet.chunk_starts.is_empty() {
12240            return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
12241        }
12242
12243        let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
12244        let mut chunk_start = sheet.chunk_starts[chunk_idx];
12245        while chunk_start.saturating_add(chunk_rows) <= row0 {
12246            chunk_idx = chunk_idx.saturating_add(1);
12247            chunk_start = chunk_start.saturating_add(chunk_rows);
12248        }
12249        (
12250            chunk_idx,
12251            chunk_start as u32,
12252            row0.saturating_sub(chunk_start),
12253        )
12254    }
12255
12256    fn locate_row_in_empty_sheet_for_computed_write_plan(
12257        row0: usize,
12258        chunk_rows: usize,
12259    ) -> (usize, u32, usize) {
12260        let chunk_rows = chunk_rows.max(1);
12261        let chunk_idx = row0 / chunk_rows;
12262        let chunk_start = chunk_idx.saturating_mul(chunk_rows);
12263        (
12264            chunk_idx,
12265            chunk_start as u32,
12266            row0.saturating_sub(chunk_start),
12267        )
12268    }
12269
12270    #[cfg(test)]
12271    pub(crate) fn debug_plan_computed_write_coalescing(
12272        &self,
12273        buffer: &ComputedWriteBuffer,
12274    ) -> ComputedWriteCoalescingPlan {
12275        self.plan_computed_write_coalescing(buffer)
12276    }
12277
12278    pub(crate) fn flush_computed_write_buffer(
12279        &mut self,
12280        buffer: &mut ComputedWriteBuffer,
12281    ) -> Result<(), ExcelError> {
12282        if buffer.is_empty() {
12283            return Ok(());
12284        }
12285
12286        // Keep ownership of all pending writes until the final request
12287        // checkpoint and bounded commit-window preflight succeed so failures
12288        // remain retry safe. The synchronization and flush that follow are
12289        // infallible mutations with no cancellation point.
12290        self.resource_checkpoint(0)?;
12291        let commit_started = self.preflight_evaluation_commit_window(buffer.len())?;
12292        let (writes, formats_present) = buffer.take_writes();
12293        let plan = self.plan_owned_computed_write_coalescing(writes, formats_present);
12294        self.flush_computed_write_plan(plan);
12295        self.observe_evaluation_commit_window(commit_started);
12296
12297        Ok(())
12298    }
12299
12300    fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
12301        for chunk in plan.chunks {
12302            self.flush_computed_write_chunk_plan(chunk);
12303        }
12304    }
12305
12306    fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
12307        match &chunk.shape {
12308            ComputedWriteChunkPlanShape::Point => {
12309                self.flush_computed_write_chunk_plan_as_points(chunk);
12310            }
12311            ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
12312                self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
12313            }
12314            ComputedWriteChunkPlanShape::DenseRange { .. } => {
12315                self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12316            }
12317            ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
12318                if Self::should_emit_computed_run_fragment(*len, *runs) {
12319                    self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
12320                } else {
12321                    self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12322                }
12323            }
12324        }
12325    }
12326
12327    #[inline]
12328    fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
12329        runs <= len / 2
12330    }
12331
12332    fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
12333        let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
12334        for entry in chunk.entries {
12335            let row0 = chunk
12336                .chunk_start_row0
12337                .saturating_add(entry.row_in_chunk as u32);
12338            self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
12339        }
12340        self.apply_computed_overlay_format_effect(
12341            chunk.sheet_id,
12342            chunk.col0,
12343            chunk.chunk_idx,
12344            chunk.format_effect,
12345        );
12346    }
12347
12348    fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
12349        &mut self,
12350        chunk: ComputedWriteChunkPlan,
12351    ) {
12352        let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
12353        let sheet_id = chunk.sheet_id;
12354        let col0 = chunk.col0;
12355        let chunk_idx = chunk.chunk_idx;
12356        let chunk_start_row0 = chunk.chunk_start_row0;
12357        let format_effect = chunk.format_effect;
12358        let items: Vec<(usize, OverlayValue)> = chunk
12359            .entries
12360            .into_iter()
12361            .map(|entry| (entry.row_in_chunk, entry.value))
12362            .collect();
12363        match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
12364            items,
12365            point_estimate,
12366        ) {
12367            Some(Ok(fragment)) => {
12368                self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
12369            }
12370            Some(Err(cells)) => {
12371                self.flush_computed_overlay_cells_as_points(
12372                    sheet_id,
12373                    col0,
12374                    chunk_start_row0,
12375                    cells,
12376                );
12377            }
12378            None => {}
12379        }
12380        self.apply_computed_overlay_format_effect(sheet_id, col0, chunk_idx, format_effect);
12381    }
12382
12383    #[inline]
12384    fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
12385        chunk
12386            .entries
12387            .iter()
12388            .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
12389            .fold(0usize, usize::saturating_add)
12390    }
12391
12392    fn flush_computed_overlay_cells_as_points(
12393        &mut self,
12394        sheet_id: SheetId,
12395        col0: u32,
12396        chunk_start_row0: u32,
12397        cells: Vec<(usize, OverlayValue)>,
12398    ) {
12399        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12400        for (row_in_chunk, value) in cells {
12401            let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
12402            self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
12403        }
12404    }
12405
12406    fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12407        if chunk.entries.is_empty() {
12408            return;
12409        }
12410        let start = chunk.entries[0].row_in_chunk;
12411        let values: Vec<OverlayValue> =
12412            chunk.entries.into_iter().map(|entry| entry.value).collect();
12413        if let Some(fragment) = OverlayFragment::dense_range(start, values) {
12414            self.apply_computed_overlay_fragment(
12415                chunk.sheet_id,
12416                chunk.col0,
12417                chunk.chunk_idx,
12418                fragment,
12419            );
12420        }
12421        self.apply_computed_overlay_format_effect(
12422            chunk.sheet_id,
12423            chunk.col0,
12424            chunk.chunk_idx,
12425            chunk.format_effect,
12426        );
12427    }
12428
12429    fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12430        if chunk.entries.is_empty() {
12431            return;
12432        }
12433        let start = chunk.entries[0].row_in_chunk;
12434        let values: Vec<OverlayValue> =
12435            chunk.entries.into_iter().map(|entry| entry.value).collect();
12436        if let Some(fragment) = OverlayFragment::run_range(start, values) {
12437            self.apply_computed_overlay_fragment(
12438                chunk.sheet_id,
12439                chunk.col0,
12440                chunk.chunk_idx,
12441                fragment,
12442            );
12443        }
12444        self.apply_computed_overlay_format_effect(
12445            chunk.sheet_id,
12446            chunk.col0,
12447            chunk.chunk_idx,
12448            chunk.format_effect,
12449        );
12450    }
12451
12452    fn apply_computed_overlay_format_effect(
12453        &mut self,
12454        sheet_id: SheetId,
12455        col0: u32,
12456        chunk_idx: usize,
12457        effect: ComputedWriteChunkFormatEffect,
12458    ) {
12459        if !(self.config.arrow_storage_enabled
12460            && self.config.delta_overlay_enabled
12461            && self.config.write_formula_overlay_enabled)
12462            || self.computed_overlay_mirroring_disabled
12463        {
12464            return;
12465        }
12466
12467        let sheet_name = self.graph.sheet_name(sheet_id);
12468        let Some(sheet) = self.arrow_sheets.sheet_mut(sheet_name) else {
12469            return;
12470        };
12471        let Some(chunk) = sheet
12472            .columns
12473            .get_mut(col0 as usize)
12474            .and_then(|column| column.chunk_mut(chunk_idx))
12475        else {
12476            return;
12477        };
12478        match effect {
12479            ComputedWriteChunkFormatEffect::NoFormatWork => {}
12480            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Range {
12481                start,
12482                end,
12483            }) => {
12484                #[cfg(test)]
12485                {
12486                    self.computed_overlay_stale_clear_range_effects_for_test = self
12487                        .computed_overlay_stale_clear_range_effects_for_test
12488                        .saturating_add(1);
12489                }
12490                chunk.computed_overlay.clear_format_range(start, end);
12491            }
12492            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Offsets(
12493                offsets,
12494            )) => {
12495                #[cfg(test)]
12496                {
12497                    self.computed_overlay_stale_clear_offset_attempts_for_test = self
12498                        .computed_overlay_stale_clear_offset_attempts_for_test
12499                        .saturating_add(offsets.len() as u64);
12500                }
12501                chunk.computed_overlay.clear_format_offsets(&offsets);
12502            }
12503            ComputedWriteChunkFormatEffect::SetExplicit(formats) => {
12504                #[cfg(test)]
12505                {
12506                    self.computed_overlay_set_explicit_entry_operations_for_test = self
12507                        .computed_overlay_set_explicit_entry_operations_for_test
12508                        .saturating_add(formats.len() as u64);
12509                }
12510                for (row_in_chunk, format_id) in formats {
12511                    chunk.computed_overlay.set_format(row_in_chunk, format_id);
12512                }
12513            }
12514        }
12515    }
12516
12517    fn apply_computed_overlay_fragment(
12518        &mut self,
12519        sheet_id: SheetId,
12520        col0: u32,
12521        chunk_idx: usize,
12522        fragment: OverlayFragment,
12523    ) {
12524        if !(self.config.arrow_storage_enabled
12525            && self.config.delta_overlay_enabled
12526            && self.config.write_formula_overlay_enabled)
12527        {
12528            return;
12529        }
12530        if self.computed_overlay_mirroring_disabled {
12531            return;
12532        }
12533
12534        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12535        self.ensure_arrow_sheet(&sheet_name);
12536
12537        let col0 = col0 as usize;
12538        let asheet = self
12539            .arrow_sheets
12540            .sheet_mut(&sheet_name)
12541            .expect("ArrowSheet must exist");
12542
12543        let cur_cols = asheet.columns.len();
12544        if col0 >= cur_cols {
12545            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12546        }
12547
12548        let start_row0 = asheet
12549            .chunk_starts
12550            .get(chunk_idx)
12551            .copied()
12552            .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
12553        let required_rows =
12554            start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
12555        if required_rows > asheet.nrows as usize {
12556            if asheet.columns.is_empty() {
12557                asheet.insert_columns(0, 1);
12558            }
12559            asheet.ensure_row_capacity(required_rows);
12560        }
12561
12562        let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
12563            return;
12564        };
12565        let delta = ch.computed_overlay.apply_fragment(fragment);
12566        self.adjust_computed_overlay_bytes(delta);
12567
12568        if let Some(cap) = self.config.max_overlay_memory_bytes
12569            && self.computed_overlay_bytes_estimate > cap
12570        {
12571            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12572        }
12573    }
12574
12575    #[inline]
12576    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
12577        if delta >= 0 {
12578            self.computed_overlay_bytes_estimate = self
12579                .computed_overlay_bytes_estimate
12580                .saturating_add(delta as usize);
12581        } else {
12582            self.computed_overlay_bytes_estimate = self
12583                .computed_overlay_bytes_estimate
12584                .saturating_sub((-delta) as usize);
12585        }
12586    }
12587
12588    fn clear_all_computed_overlays(&mut self) {
12589        let mut freed_total = 0usize;
12590        for sh in self.arrow_sheets.sheets.iter_mut() {
12591            for col in sh.columns.iter_mut() {
12592                for ch in col.chunks.iter_mut() {
12593                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12594                }
12595                for ch in col.sparse_chunks.values_mut() {
12596                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12597                }
12598            }
12599        }
12600        self.computed_overlay_bytes_estimate = self
12601            .computed_overlay_bytes_estimate
12602            .saturating_sub(freed_total);
12603    }
12604
12605    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
12606        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
12607        // Handle budget pressure by compacting computed overlays into base lanes.
12608        self.compact_all_computed_overlays();
12609    }
12610
12611    /// Fold all computed overlay entries across all sheets into their base arrays.
12612    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
12613    fn compact_all_computed_overlays(&mut self) {
12614        let mut freed_total = 0usize;
12615        for sheet in self.arrow_sheets.sheets.iter_mut() {
12616            for col_idx in 0..sheet.columns.len() {
12617                // Dense chunks
12618                let num_dense = sheet.columns[col_idx].chunks.len();
12619                for ch_idx in 0..num_dense {
12620                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
12621                }
12622                // Sparse chunks
12623                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
12624                    .sparse_chunks
12625                    .keys()
12626                    .copied()
12627                    .collect();
12628                for ch_idx in sparse_keys {
12629                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
12630                }
12631            }
12632        }
12633        self.computed_overlay_bytes_estimate = self
12634            .computed_overlay_bytes_estimate
12635            .saturating_sub(freed_total);
12636        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
12637    }
12638
12639    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
12640        let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
12641    }
12642
12643    fn record_vertex_value_to_overlay(
12644        &mut self,
12645        vertex_id: VertexId,
12646        value: &LiteralValue,
12647        computed_writes: Option<&mut ComputedWriteBuffer>,
12648    ) -> Result<(), ExcelError> {
12649        if !(self.config.arrow_storage_enabled
12650            && self.config.delta_overlay_enabled
12651            && self.config.write_formula_overlay_enabled)
12652        {
12653            return Ok(());
12654        }
12655        if self.computed_overlay_mirroring_disabled {
12656            return Ok(());
12657        }
12658        if !matches!(
12659            self.graph.get_vertex_kind(vertex_id),
12660            VertexKind::FormulaScalar | VertexKind::FormulaArray
12661        ) {
12662            return Ok(());
12663        }
12664        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
12665            return Ok(());
12666        };
12667        let Some(buffer) = computed_writes else {
12668            // Unbuffered: one sheet lookup for the value and its format.
12669            let format_id = self.derived_formats.get(&cell);
12670            self.write_computed_cell_0based(cell, value, format_id);
12671            return Ok(());
12672        };
12673        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
12674        let date_system = self.arrow_sheet_date_system(&sheet_name);
12675        let ov = Self::literal_to_overlay_value(value, date_system);
12676        {
12677            let format_id = self.derived_formats.get(&cell);
12678            buffer.push_cell_with_format(
12679                cell.sheet_id,
12680                cell.coord.row(),
12681                cell.coord.col(),
12682                ov,
12683                format_id,
12684            );
12685            if self.should_flush_computed_write_buffer(buffer) {
12686                self.flush_computed_write_buffer(buffer)?;
12687            }
12688        }
12689        Ok(())
12690    }
12691
12692    #[inline]
12693    fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
12694        self.config.max_overlay_memory_bytes.is_some_and(|cap| {
12695            if cap == 0 {
12696                return false;
12697            }
12698            self.computed_overlay_bytes_estimate
12699                .saturating_add(buffer.estimated_bytes())
12700                > cap
12701        })
12702    }
12703
12704    /// Estimated memory usage for computed overlays (formula/spill mirroring).
12705    pub fn overlay_memory_usage(&self) -> usize {
12706        self.computed_overlay_bytes_estimate
12707    }
12708
12709    #[cfg(test)]
12710    pub(crate) fn debug_overlay_compactions(&self) -> u64 {
12711        self.overlay_compactions
12712    }
12713
12714    #[cfg(test)]
12715    pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
12716        let mut total = 0usize;
12717        for sheet in &self.arrow_sheets.sheets {
12718            for column in &sheet.columns {
12719                for chunk in &column.chunks {
12720                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
12721                }
12722                for chunk in column.sparse_chunks.values() {
12723                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
12724                }
12725            }
12726        }
12727        self.computed_overlay_bytes_estimate = total;
12728        total
12729    }
12730
12731    fn resolve_sheet_locator_for_write(
12732        &mut self,
12733        loc: formualizer_common::SheetLocator<'_>,
12734        current_sheet: &str,
12735    ) -> Result<SheetId, ExcelError> {
12736        Ok(match loc {
12737            formualizer_common::SheetLocator::Id(id) => id,
12738            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
12739            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
12740        })
12741    }
12742
12743    fn resolve_sheet_locator_for_read(
12744        &self,
12745        loc: formualizer_common::SheetLocator<'_>,
12746        current_sheet: &str,
12747    ) -> Result<SheetId, ExcelError> {
12748        match loc {
12749            formualizer_common::SheetLocator::Id(id) => Ok(id),
12750            formualizer_common::SheetLocator::Name(name) => self
12751                .graph
12752                .sheet_id(name.as_ref())
12753                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
12754            formualizer_common::SheetLocator::Current => self
12755                .graph
12756                .sheet_id(current_sheet)
12757                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
12758        }
12759    }
12760
12761    /// Set a cell value
12762    pub fn set_cell_value(
12763        &mut self,
12764        sheet: &str,
12765        row: u32,
12766        col: u32,
12767        value: LiteralValue,
12768    ) -> Result<(), ExcelError> {
12769        self.observe_function_semantic_epoch()?;
12770        let sheet_existed = self.graph.sheet_id(sheet).is_some();
12771        let sheet_id = self.graph.sheet_id_mut(sheet);
12772        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
12773        let replaced_formula =
12774            self.graph
12775                .get_vertex_id_for_address(&cell_ref)
12776                .is_some_and(|vertex| {
12777                    matches!(
12778                        self.graph.get_vertex_kind(vertex),
12779                        VertexKind::FormulaScalar | VertexKind::FormulaArray
12780                    )
12781                });
12782        self.graph.set_cell_value(sheet, row, col, value.clone())?;
12783        self.clear_cell_format_state(sheet, cell_ref);
12784        self.record_changed_cell(sheet, row, col);
12785        if !sheet_existed || replaced_formula {
12786            self.mark_topology_edited();
12787        }
12788        // Mirror into Arrow overlay when enabled
12789        self.mirror_value_to_overlay(sheet, row, col, &value);
12790        // Advance snapshot to reflect external mutation.
12791        self.mark_data_edited();
12792        Ok(())
12793    }
12794
12795    /// Record a single-cell change: invalidates pending spills blocked on it.
12796    fn record_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
12797        let sheet_id = self.graph.sheet_id_mut(sheet);
12798        self.record_structural_change(StructuralScope::Cell {
12799            sheet: sheet_id,
12800            row: row.saturating_sub(1),
12801            col: col.saturating_sub(1),
12802        });
12803    }
12804
12805    fn record_change_for_event(&mut self, event: &ChangeEvent) {
12806        match event {
12807            ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
12808                self.record_structural_change(StructuralScope::Cell {
12809                    sheet: addr.sheet_id,
12810                    row: addr.coord.row(),
12811                    col: addr.coord.col(),
12812                });
12813            }
12814            ChangeEvent::SpillCommitted { new, .. } => {
12815                if let Some(scope) = Self::structural_scope_from_cells(&new.target_cells) {
12816                    self.record_structural_change(scope);
12817                }
12818            }
12819            ChangeEvent::SpillCleared { old, .. } => {
12820                if let Some(scope) = Self::structural_scope_from_cells(&old.target_cells) {
12821                    self.record_structural_change(scope);
12822                }
12823            }
12824            ChangeEvent::DefineName { .. }
12825            | ChangeEvent::UpdateName { .. }
12826            | ChangeEvent::DeleteName { .. }
12827            | ChangeEvent::NamedRangeAdjusted { .. } => {
12828                // Direct name events are preflighted by the logged-name APIs.
12829                // Structural entry points preflight spans before emitting a
12830                // NamedRangeAdjusted event. Epoch changes only rebuild caches.
12831                self.record_structural_change(StructuralScope::AllSheets);
12832            }
12833            ChangeEvent::VertexMoved { .. } | ChangeEvent::FormulaAdjusted { .. } => {
12834                // Structural entry points publish their axis delta once after
12835                // the graph and Arrow commits.
12836            }
12837            ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
12838                self.record_structural_change(StructuralScope::Region(Region::whole_row(
12839                    *sheet_id, *row0,
12840                )));
12841            }
12842            ChangeEvent::AddVertex { .. }
12843            | ChangeEvent::RemoveVertex { .. }
12844            | ChangeEvent::EdgeAdded { .. }
12845            | ChangeEvent::EdgeRemoved { .. }
12846            | ChangeEvent::CompoundStart { .. }
12847            | ChangeEvent::CompoundEnd { .. }
12848            | ChangeEvent::StagedFormulaCellChanged { .. } => {}
12849        }
12850    }
12851
12852    fn record_structural_change(&mut self, scope: StructuralScope) {
12853        self.invalidate_pending_spills(scope);
12854    }
12855
12856    fn structural_scope_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
12857        let first = cells.first()?;
12858        let sheet_id = first.sheet_id;
12859        if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
12860            return Some(StructuralScope::OpaqueGlobal);
12861        }
12862        let mut row_start = first.coord.row();
12863        let mut row_end = row_start;
12864        let mut col_start = first.coord.col();
12865        let mut col_end = col_start;
12866        for cell in cells.iter().skip(1) {
12867            row_start = row_start.min(cell.coord.row());
12868            row_end = row_end.max(cell.coord.row());
12869            col_start = col_start.min(cell.coord.col());
12870            col_end = col_end.max(cell.coord.col());
12871        }
12872        Some(StructuralScope::Region(Region::rect(
12873            sheet_id, row_start, row_end, col_start, col_end,
12874        )))
12875    }
12876
12877    pub fn set_cell_value_ref(
12878        &mut self,
12879        cell: formualizer_common::SheetCellRef<'_>,
12880        current_sheet: &str,
12881        value: LiteralValue,
12882    ) -> Result<(), ExcelError> {
12883        let owned = cell.into_owned();
12884        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
12885        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12886        self.set_cell_value(
12887            &sheet_name,
12888            owned.coord.row() + 1,
12889            owned.coord.col() + 1,
12890            value,
12891        )
12892    }
12893
12894    pub fn set_cell_formula_ref(
12895        &mut self,
12896        cell: formualizer_common::SheetCellRef<'_>,
12897        current_sheet: &str,
12898        ast: ASTNode,
12899    ) -> Result<(), ExcelError> {
12900        let owned = cell.into_owned();
12901        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
12902        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12903        self.set_cell_formula(
12904            &sheet_name,
12905            owned.coord.row() + 1,
12906            owned.coord.col() + 1,
12907            ast,
12908        )
12909    }
12910
12911    pub fn get_cell_value_ref(
12912        &self,
12913        cell: formualizer_common::SheetCellRef<'_>,
12914        current_sheet: &str,
12915    ) -> Result<Option<LiteralValue>, ExcelError> {
12916        let owned = cell.into_owned();
12917        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
12918        let sheet_name = self.graph.sheet_name(sheet_id);
12919        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
12920    }
12921
12922    pub fn resolve_range_view_sheet_ref<'c>(
12923        &'c self,
12924        r: &formualizer_common::SheetRef<'_>,
12925        current_sheet: &str,
12926    ) -> Result<RangeView<'c>, ExcelError> {
12927        use formualizer_common::SheetLocator;
12928
12929        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
12930            match loc {
12931                SheetLocator::Current => Ok(None),
12932                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
12933                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
12934            }
12935        };
12936
12937        let rt = match r {
12938            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
12939                sheet: sheet_to_opt_name(cell.sheet.clone())?,
12940                row: cell.coord.row() + 1,
12941                col: cell.coord.col() + 1,
12942                row_abs: cell.coord.row_abs(),
12943                col_abs: cell.coord.col_abs(),
12944            },
12945            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
12946                sheet: sheet_to_opt_name(range.sheet.clone())?,
12947                start_row: range.start_row.map(|b| b.index + 1),
12948                start_col: range.start_col.map(|b| b.index + 1),
12949                end_row: range.end_row.map(|b| b.index + 1),
12950                end_col: range.end_col.map(|b| b.index + 1),
12951                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
12952                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
12953                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
12954                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
12955            },
12956        };
12957
12958        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
12959    }
12960
12961    /// Set a cell formula
12962    pub fn set_cell_formula(
12963        &mut self,
12964        sheet: &str,
12965        row: u32,
12966        col: u32,
12967        ast: ASTNode,
12968    ) -> Result<(), ExcelError> {
12969        self.observe_function_semantic_epoch()?;
12970        let sheet_id = self.graph.sheet_id_mut(sheet);
12971        let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
12972        let ingested = {
12973            let mut pipeline = self.ingest_pipeline();
12974            pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
12975        };
12976        self.graph.set_cell_formula_with_plan(
12977            sheet,
12978            row,
12979            col,
12980            ingested.ast_id,
12981            &ingested.dep_plan,
12982            ingested.dep_plan.volatile,
12983            ingested.dep_plan.dynamic,
12984        )?;
12985        self.clear_cell_format_state(sheet, placement);
12986        self.record_changed_cell(sheet, row, col);
12987
12988        // If the cell previously held a user value in the delta overlay, it must not continue
12989        // to mask the formula result under Arrow-canonical reads (overlay precedence is
12990        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
12991        // because an explicit `Empty` overlay would still take precedence over computed values.
12992        self.clear_delta_overlay_cell(sheet, row, col);
12993
12994        // Advance snapshot to reflect external mutation
12995        self.mark_topology_edited();
12996        Ok(())
12997    }
12998
12999    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
13000    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
13001    where
13002        I: IntoIterator<Item = (u32, u32, ASTNode)>,
13003    {
13004        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
13005        let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
13006        let sheet_id = self.graph.sheet_id_mut(sheet);
13007        let ingested = {
13008            let mut pipeline = self.ingest_pipeline();
13009            let inputs = collected.into_iter().map(|(row, col, ast)| {
13010                let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13011                (FormulaAstInput::Tree(ast), placement, None)
13012            });
13013            pipeline.ingest_batch(inputs)?
13014        };
13015        let planned: Vec<(u32, u32, AstNodeId, DependencyPlanRow)> = ingested
13016            .into_iter()
13017            .map(|formula| {
13018                (
13019                    formula.placement.coord.row() + 1,
13020                    formula.placement.coord.col() + 1,
13021                    formula.ast_id,
13022                    formula.dep_plan,
13023                )
13024            })
13025            .collect();
13026        let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
13027        for (row, col) in edited_cells {
13028            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13029            self.clear_cell_format_state(sheet, cell);
13030            self.record_changed_cell(sheet, row, col);
13031        }
13032        // Single topology bump after batch
13033        if n > 0 {
13034            self.mark_topology_edited();
13035        }
13036        Ok(n)
13037    }
13038
13039    #[inline]
13040    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
13041        match v {
13042            LiteralValue::Empty => None,
13043            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
13044            other => Some(other),
13045        }
13046    }
13047
13048    fn materialize_temporal_egress(
13049        value: LiteralValue,
13050        class: Option<&formualizer_common::numfmt::FormatClass>,
13051        policy: crate::engine::TemporalEgress,
13052        date_system: crate::engine::DateSystem,
13053    ) -> LiteralValue {
13054        use formualizer_common::numfmt::FormatClass;
13055        if policy == crate::engine::TemporalEgress::Serial {
13056            return value;
13057        }
13058        let LiteralValue::Number(serial) = value else {
13059            return value;
13060        };
13061        match class {
13062            Some(FormatClass::Date) => {
13063                formualizer_common::try_serial_to_date_for(date_system, serial)
13064                    .map(LiteralValue::Date)
13065                    .unwrap_or(LiteralValue::Number(serial))
13066            }
13067            Some(FormatClass::DateTime) => {
13068                formualizer_common::try_serial_to_datetime_for(date_system, serial)
13069                    .map(LiteralValue::DateTime)
13070                    .unwrap_or(LiteralValue::Number(serial))
13071            }
13072            Some(FormatClass::Time) => {
13073                let seconds = (serial.rem_euclid(1.0) * 86_400.0).round() as u32 % 86_400;
13074                chrono::NaiveTime::from_num_seconds_from_midnight_opt(seconds, 0)
13075                    .map(LiteralValue::Time)
13076                    .unwrap_or(LiteralValue::Number(serial))
13077            }
13078            Some(FormatClass::Duration) => {
13079                let nanos = (serial * 86_400.0 * 1_000_000_000.0).round();
13080                if nanos.is_finite() && nanos >= i64::MIN as f64 && nanos <= i64::MAX as f64 {
13081                    LiteralValue::Duration(chrono::Duration::nanoseconds(nanos as i64))
13082                } else {
13083                    LiteralValue::Number(serial)
13084                }
13085            }
13086            _ => LiteralValue::Number(serial),
13087        }
13088    }
13089
13090    pub(crate) fn effective_format_id(
13091        &self,
13092        sheet: &str,
13093        row: u32,
13094        col: u32,
13095    ) -> Option<crate::format::FormatId> {
13096        let arrow = self.arrow_sheets.sheet(sheet).and_then(|arrow| {
13097            arrow.format_id(
13098                row.saturating_sub(1) as usize,
13099                col.saturating_sub(1) as usize,
13100            )
13101        });
13102        arrow.or_else(|| {
13103            let sheet_id = self.graph.sheet_id(sheet)?;
13104            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13105            self.derived_formats.get(&cell)
13106        })
13107    }
13108
13109    /// Get a cell value through the single temporal egress boundary.
13110    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13111        let raw = self.read_cell_value(sheet, row, col)?;
13112        let format = self.effective_format_id(sheet, row, col);
13113        let class = format.and_then(|id| self.format_registry.class(id));
13114        Self::normalize_public_cell_read(Self::materialize_temporal_egress(
13115            raw,
13116            class,
13117            self.config.temporal_egress,
13118            self.config.date_system,
13119        ))
13120    }
13121
13122    /// Read a rectangular range through the temporal egress boundary.
13123    pub fn get_range_values(
13124        &self,
13125        sheet: &str,
13126        sr: u32,
13127        sc: u32,
13128        er: u32,
13129        ec: u32,
13130    ) -> Vec<Vec<LiteralValue>> {
13131        let height = er.saturating_sub(sr).saturating_add(1) as usize;
13132        let width = ec.saturating_sub(sc).saturating_add(1) as usize;
13133        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13134            return vec![vec![LiteralValue::Empty; width]; height];
13135        };
13136        let view = asheet.range_view(
13137            sr.saturating_sub(1) as usize,
13138            sc.saturating_sub(1) as usize,
13139            er.saturating_sub(1) as usize,
13140            ec.saturating_sub(1) as usize,
13141        );
13142        let sheet_id = self.graph.sheet_id(sheet);
13143        let derived_formats = &self.derived_formats;
13144        let has_derived_formats =
13145            sheet_id.is_some_and(|sheet_id| derived_formats.any(|cell| cell.sheet_id == sheet_id));
13146        let mut out = Vec::with_capacity(height);
13147        if !asheet.has_formats() && !has_derived_formats {
13148            for rr in 0..height {
13149                let mut row = Vec::with_capacity(width);
13150                for cc in 0..width {
13151                    row.push(view.get_cell(rr, cc));
13152                }
13153                out.push(row);
13154            }
13155            return out;
13156        }
13157        let format_registry = &self.format_registry;
13158        for rr in 0..height {
13159            let mut row = Vec::with_capacity(width);
13160            for cc in 0..width {
13161                let raw = view.get_cell(rr, cc);
13162                let row0 = sr.saturating_sub(1).saturating_add(rr as u32);
13163                let col0 = sc.saturating_sub(1).saturating_add(cc as u32);
13164                let format = asheet.format_id(row0 as usize, col0 as usize).or_else(|| {
13165                    let cell = CellRef::new(sheet_id?, Coord::new(row0, col0, true, true));
13166                    derived_formats.get(&cell)
13167                });
13168                let class = format.and_then(|id| format_registry.class(id));
13169                row.push(Self::materialize_temporal_egress(
13170                    raw,
13171                    class,
13172                    self.config.temporal_egress,
13173                    self.config.date_system,
13174                ));
13175            }
13176            out.push(row);
13177        }
13178        out
13179    }
13180
13181    /// Unified internal read API for a single cell value (Arrow-truth).
13182    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13183        let asheet = self.sheet_store().sheet(sheet)?;
13184        let r0 = row.saturating_sub(1) as usize;
13185        let c0 = col.saturating_sub(1) as usize;
13186        let v = asheet.get_cell_value(r0, c0);
13187        if matches!(v, LiteralValue::Empty) {
13188            None
13189        } else {
13190            Some(v)
13191        }
13192    }
13193
13194    /// Unified internal read API for a range of cell values (Arrow-truth).
13195    pub(crate) fn read_range_values(
13196        &self,
13197        sheet: &str,
13198        sr: u32,
13199        sc: u32,
13200        er: u32,
13201        ec: u32,
13202    ) -> RangeView<'_> {
13203        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13204            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
13205        };
13206        if er < sr || ec < sc {
13207            return asheet.range_view(1, 1, 0, 0);
13208        }
13209        let sr0 = sr.saturating_sub(1) as usize;
13210        let sc0 = sc.saturating_sub(1) as usize;
13211        let er0 = er.saturating_sub(1) as usize;
13212        let ec0 = ec.saturating_sub(1) as usize;
13213        asheet.range_view(sr0, sc0, er0, ec0)
13214    }
13215
13216    /// Get formula AST (if any) and current stored value for a cell
13217    pub fn get_cell(
13218        &self,
13219        sheet: &str,
13220        row: u32,
13221        col: u32,
13222    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
13223        let v = self.get_cell_value(sheet, row, col);
13224        let sheet_id = self.graph.sheet_id(sheet)?;
13225        let coord = Coord::from_excel(row, col, true, true);
13226        let cell = CellRef::new(sheet_id, coord);
13227        if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
13228            let ast = self.graph.get_formula(vid);
13229            Some((ast, v))
13230        } else if v.is_some() || self.graph.had_legacy_cell_vertex(&cell) {
13231            // A referenced or emptied value cell has no vertex (decision
13232            // 27), but it is a cell the graph knows, as it was when it had
13233            // one (the interactive formula edit routes on this).
13234            Some((None, v))
13235        } else {
13236            None
13237        }
13238    }
13239
13240    /// Begin batch operations - defer CSR rebuilds for better performance
13241    pub fn begin_batch(&mut self) {
13242        self.graph.begin_batch();
13243    }
13244
13245    /// End batch operations and trigger CSR rebuild
13246    pub fn end_batch(&mut self) {
13247        self.graph.end_batch();
13248    }
13249
13250    /// Begin a deferred-dirty scope for a multi-edit batch: while active,
13251    /// every edit's dirty propagation queues its sources instead of running
13252    /// a full BFS per edit, and the outermost `end_deferred_dirty` flushes
13253    /// the union with ONE multi-source propagation (O(component) instead of
13254    /// O(edits × component)). See `DependencyGraph::begin_deferred_dirty`.
13255    ///
13256    /// Callers MUST run `end_deferred_dirty` on every exit path, including
13257    /// error returns; evaluation entry points `debug_assert` no scope leaked.
13258    pub fn begin_deferred_dirty(&mut self) {
13259        self.graph.begin_deferred_dirty();
13260    }
13261
13262    /// End a deferred-dirty scope, flushing the queued propagation when the
13263    /// outermost scope closes. See `Engine::begin_deferred_dirty`.
13264    pub fn end_deferred_dirty(&mut self) {
13265        let _ = self.graph.end_deferred_dirty();
13266    }
13267
13268    /// Total vertices processed by dirty-propagation BFS loops since graph
13269    /// creation. Perf-shape observability only (cross-crate tests assert
13270    /// batched edits propagate O(component), not O(edits × component)).
13271    pub fn dirty_propagation_visits(&self) -> u64 {
13272        self.graph.dirty_propagation_visits()
13273    }
13274
13275    /// Evaluate a single vertex.
13276    /// This is the core of the sequential evaluation logic for Milestone 3.1.
13277    #[inline]
13278    fn record_cell_if_changed(
13279        delta: &mut DeltaCollector,
13280        cell: &CellRef,
13281        old: &LiteralValue,
13282        new: &LiteralValue,
13283    ) {
13284        if old != new {
13285            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
13286        }
13287    }
13288
13289    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
13290        self.observe_evaluation_resource_request(EvaluationRequestKind::Vertex, |engine| {
13291            engine.observe_function_semantic_epoch()?;
13292            // A direct request selects exactly one vertex, regardless of its formula kind.
13293            engine.resource_checkpoint(1)?;
13294            if !engine.graph.vertex_exists(vertex_id) {
13295                return engine.evaluate_vertex_impl(vertex_id, None);
13296            }
13297            let is_formula = matches!(
13298                engine.graph.get_vertex_kind(vertex_id),
13299                VertexKind::FormulaScalar | VertexKind::FormulaArray
13300            );
13301            if is_formula {
13302                engine.begin_evaluation_request();
13303                #[cfg(any(test, feature = "legacy_oracle"))]
13304                engine.graph.flush_pending_edge_deltas();
13305                let roots = [crate::engine::target_preparation::TargetProducer::Legacy(
13306                    vertex_id,
13307                )];
13308                engine.evaluate_legacy_target_roots(&roots, None)?;
13309            }
13310            engine.evaluate_vertex_impl(vertex_id, None)
13311        })
13312    }
13313
13314    fn evaluate_vertex_impl(
13315        &mut self,
13316        vertex_id: VertexId,
13317        delta: Option<&mut DeltaCollector>,
13318    ) -> Result<LiteralValue, ExcelError> {
13319        // Preserve the direct evaluator's compatibility behavior for invalid IDs, literal cells,
13320        // names, and other non-formula vertices. Only formula publication needs the C1a final
13321        // deadline checkpoint and effects pipeline.
13322        if !self.graph.vertex_exists(vertex_id) {
13323            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
13324                .with_message(format!("Vertex not found: {vertex_id:?}")));
13325        }
13326        if self.active_resource_ledger.is_some()
13327            && matches!(
13328                self.graph.get_vertex_kind(vertex_id),
13329                VertexKind::FormulaScalar | VertexKind::FormulaArray
13330            )
13331        {
13332            let value = self
13333                .evaluate_vertex_immutable(vertex_id)
13334                .unwrap_or_else(LiteralValue::Error);
13335            let effects = self.plan_vertex_effects(vertex_id, value.clone(), None)?;
13336            // Do not publish the selected result until the outer request's deadline succeeds.
13337            self.resource_checkpoint(0)?;
13338            let mut delta = delta;
13339            for effect in &effects {
13340                self.apply_effect_with_computed_writes(effect, delta.as_deref_mut(), None, None)?;
13341            }
13342            return Ok(value);
13343        }
13344
13345        let mut delta = delta;
13346
13347        // Get vertex kind and check if it needs evaluation
13348        let kind = self.graph.get_vertex_kind(vertex_id);
13349        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
13350
13351        let view = match kind {
13352            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
13353                if let Some(view) = self.graph.formula_view(vertex_id) {
13354                    view
13355                } else {
13356                    return Ok(LiteralValue::Number(0.0));
13357                }
13358            }
13359            VertexKind::Empty | VertexKind::Cell => {
13360                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
13361                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13362                    let row = cell_ref.coord.row() + 1;
13363                    let col = cell_ref.coord.col() + 1;
13364                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
13365                        return Ok(v);
13366                    }
13367                }
13368                return Ok(LiteralValue::Number(0.0));
13369            }
13370            VertexKind::NamedScalar => {
13371                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
13372                return Ok(value);
13373            }
13374            VertexKind::NamedArray => {
13375                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
13376                return Ok(value);
13377            }
13378            VertexKind::InfiniteRange
13379            | VertexKind::Range
13380            | VertexKind::External
13381            | VertexKind::Table => {
13382                // Not directly evaluatable here.
13383                return Ok(LiteralValue::Number(0.0));
13384            }
13385        };
13386
13387        // The interpreter uses a reference to the engine as the context.
13388        let sheet_name = self.graph.sheet_name(sheet_id);
13389        let cell_ref = self
13390            .graph
13391            .get_cell_ref(vertex_id)
13392            .expect("cell ref for vertex");
13393        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13394
13395        let result = interpreter.evaluate_formula_view(
13396            view,
13397            self.graph.data_store(),
13398            self.graph.sheet_reg(),
13399        );
13400
13401        // If array result, perform spill from the anchor cell
13402        match result {
13403            Ok(cv) => {
13404                let derived_format = cv.format_id();
13405                self.record_derived_format(vertex_id, derived_format);
13406                let result_literal =
13407                    crate::engine::result_finalization::finalize_formula_result(cv.into_literal());
13408                let output_sheet_name = sheet_name.to_string();
13409                self.write_computed_overlay_format_0based(
13410                    &output_sheet_name,
13411                    cell_ref.coord.row(),
13412                    cell_ref.coord.col(),
13413                    derived_format,
13414                );
13415                match result_literal {
13416                    LiteralValue::Array(rows) => {
13417                        // Update kind to FormulaArray for tracking
13418                        self.graph
13419                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
13420                        // Build target cells rectangle starting from anchor
13421                        let anchor = self
13422                            .graph
13423                            .get_cell_ref(vertex_id)
13424                            .expect("cell ref for vertex");
13425                        let sheet_id = anchor.sheet_id;
13426                        let h = rows.len() as u32;
13427                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
13428
13429                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
13430                        let spill_cells = (h as u64).saturating_mul(w as u64);
13431                        if spill_cells > self.config.spill.max_spill_cells as u64 {
13432                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
13433                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13434                                .with_message("SpillTooLarge")
13435                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13436                                    expected_rows: h,
13437                                    expected_cols: w,
13438                                });
13439                            let spill_val = LiteralValue::Error(spill_err.clone());
13440                            if let Some(d) = delta.as_deref_mut() {
13441                                let old = self
13442                                    .read_cell_value(
13443                                        self.graph.sheet_name(anchor.sheet_id),
13444                                        anchor.coord.row() + 1,
13445                                        anchor.coord.col() + 1,
13446                                    )
13447                                    .unwrap_or(LiteralValue::Empty);
13448                                if old != spill_val {
13449                                    d.record_cell(
13450                                        anchor.sheet_id,
13451                                        anchor.coord.row(),
13452                                        anchor.coord.col(),
13453                                    );
13454                                }
13455                            }
13456                            self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13457                            if self.config.arrow_storage_enabled
13458                                && self.config.delta_overlay_enabled
13459                                && self.config.write_formula_overlay_enabled
13460                            {
13461                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13462                                self.mirror_value_to_computed_overlay(
13463                                    &sheet_name,
13464                                    anchor.coord.row() + 1,
13465                                    anchor.coord.col() + 1,
13466                                    &spill_val,
13467                                );
13468                            }
13469                            return Ok(spill_val);
13470                        }
13471                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
13472                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
13473                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
13474                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
13475                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
13476                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
13477                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
13478                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13479                                .with_message("Spill exceeds sheet bounds")
13480                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13481                                    expected_rows: h,
13482                                    expected_cols: w,
13483                                });
13484                            let spill_val = LiteralValue::Error(spill_err.clone());
13485                            if let Some(d) = delta.as_deref_mut() {
13486                                let old = self
13487                                    .read_cell_value(
13488                                        self.graph.sheet_name(anchor.sheet_id),
13489                                        anchor.coord.row() + 1,
13490                                        anchor.coord.col() + 1,
13491                                    )
13492                                    .unwrap_or(LiteralValue::Empty);
13493                                if old != spill_val {
13494                                    d.record_cell(
13495                                        anchor.sheet_id,
13496                                        anchor.coord.row(),
13497                                        anchor.coord.col(),
13498                                    );
13499                                }
13500                            }
13501                            self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13502                            if self.config.arrow_storage_enabled
13503                                && self.config.delta_overlay_enabled
13504                                && self.config.write_formula_overlay_enabled
13505                            {
13506                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13507                                self.mirror_value_to_computed_overlay(
13508                                    &sheet_name,
13509                                    anchor.coord.row() + 1,
13510                                    anchor.coord.col() + 1,
13511                                    &spill_val,
13512                                );
13513                            }
13514                            return Ok(spill_val);
13515                        }
13516                        let mut targets = Vec::new();
13517                        for r in 0..h {
13518                            for c in 0..w {
13519                                targets.push(self.graph.make_cell_ref_internal(
13520                                    sheet_id,
13521                                    anchor.coord.row() + r,
13522                                    anchor.coord.col() + c,
13523                                ));
13524                            }
13525                        }
13526
13527                        // Plan spill via spill manager shim
13528                        match self.spill_mgr.reserve(
13529                            vertex_id,
13530                            anchor,
13531                            SpillShape { rows: h, cols: w },
13532                            SpillMeta {
13533                                epoch: self.recalc_epoch,
13534                                config: self.config.spill,
13535                            },
13536                        ) {
13537                            Ok(()) => {
13538                                // Commit: write values to grid
13539                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
13540                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
13541                                if let Err(e) = self.commit_spill_and_mirror(
13542                                    vertex_id,
13543                                    &targets,
13544                                    rows.clone(),
13545                                    delta.as_deref_mut(),
13546                                    None,
13547                                ) {
13548                                    if e.kind != ExcelErrorKind::Spill {
13549                                        return Err(e);
13550                                    }
13551                                    // If commit fails, mark as error
13552                                    self.clear_spill_projection_and_mirror(
13553                                        vertex_id,
13554                                        delta.as_deref_mut(),
13555                                    );
13556                                    if let Some(d) = delta.as_deref_mut() {
13557                                        let old = self
13558                                            .read_cell_value(
13559                                                self.graph.sheet_name(anchor.sheet_id),
13560                                                anchor.coord.row() + 1,
13561                                                anchor.coord.col() + 1,
13562                                            )
13563                                            .unwrap_or(LiteralValue::Empty);
13564                                        let new = LiteralValue::Error(e.clone());
13565                                        if old != new {
13566                                            d.record_cell(
13567                                                anchor.sheet_id,
13568                                                anchor.coord.row(),
13569                                                anchor.coord.col(),
13570                                            );
13571                                        }
13572                                    }
13573                                    let err_val = LiteralValue::Error(e.clone());
13574                                    self.graph.update_vertex_value_ref(vertex_id, &err_val);
13575                                    if self.config.arrow_storage_enabled
13576                                        && self.config.delta_overlay_enabled
13577                                        && self.config.write_formula_overlay_enabled
13578                                    {
13579                                        let sheet_name =
13580                                            self.graph.sheet_name(anchor.sheet_id).to_string();
13581                                        self.mirror_value_to_computed_overlay(
13582                                            &sheet_name,
13583                                            anchor.coord.row() + 1,
13584                                            anchor.coord.col() + 1,
13585                                            &err_val,
13586                                        );
13587                                    }
13588                                    return Ok(err_val);
13589                                }
13590                                // Anchor shows the top-left value, like Excel
13591                                let top_left = rows
13592                                    .first()
13593                                    .and_then(|r| r.first())
13594                                    .cloned()
13595                                    .unwrap_or(LiteralValue::Empty);
13596                                self.graph.update_vertex_value_ref(vertex_id, &top_left);
13597                                Ok(top_left)
13598                            }
13599                            Err(e) => {
13600                                self.clear_spill_projection_and_mirror(
13601                                    vertex_id,
13602                                    delta.as_deref_mut(),
13603                                );
13604                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13605                                    .with_message(
13606                                        e.message.unwrap_or_else(|| "Spill blocked".to_string()),
13607                                    )
13608                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13609                                        expected_rows: h,
13610                                        expected_cols: w,
13611                                    });
13612                                let spill_val = LiteralValue::Error(spill_err.clone());
13613                                if let Some(d) = delta.as_deref_mut() {
13614                                    let old = self
13615                                        .read_cell_value(
13616                                            self.graph.sheet_name(anchor.sheet_id),
13617                                            anchor.coord.row() + 1,
13618                                            anchor.coord.col() + 1,
13619                                        )
13620                                        .unwrap_or(LiteralValue::Empty);
13621                                    if old != spill_val {
13622                                        d.record_cell(
13623                                            anchor.sheet_id,
13624                                            anchor.coord.row(),
13625                                            anchor.coord.col(),
13626                                        );
13627                                    }
13628                                }
13629                                self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13630                                if self.config.arrow_storage_enabled
13631                                    && self.config.delta_overlay_enabled
13632                                    && self.config.write_formula_overlay_enabled
13633                                {
13634                                    let sheet_name =
13635                                        self.graph.sheet_name(anchor.sheet_id).to_string();
13636                                    self.mirror_value_to_computed_overlay(
13637                                        &sheet_name,
13638                                        anchor.coord.row() + 1,
13639                                        anchor.coord.col() + 1,
13640                                        &spill_val,
13641                                    );
13642                                }
13643                                Ok(spill_val)
13644                            }
13645                        }
13646                    }
13647                    other => {
13648                        // Scalar result: store value and ensure any previous spill is cleared
13649                        let spill_cells = self
13650                            .graph
13651                            .spill_cells_for_anchor(vertex_id)
13652                            .map(|cells| cells.to_vec())
13653                            .unwrap_or_default();
13654                        if let Some(d) = delta.as_deref_mut()
13655                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
13656                        {
13657                            if spill_cells.is_empty() {
13658                                let old = self
13659                                    .read_cell_value(
13660                                        self.graph.sheet_name(anchor.sheet_id),
13661                                        anchor.coord.row() + 1,
13662                                        anchor.coord.col() + 1,
13663                                    )
13664                                    .unwrap_or(LiteralValue::Empty);
13665                                if old != other {
13666                                    d.record_cell(
13667                                        anchor.sheet_id,
13668                                        anchor.coord.row(),
13669                                        anchor.coord.col(),
13670                                    );
13671                                }
13672                            } else {
13673                                for cell in spill_cells.iter() {
13674                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
13675                                    let old = self
13676                                        .get_cell_value(
13677                                            sheet_name,
13678                                            cell.coord.row() + 1,
13679                                            cell.coord.col() + 1,
13680                                        )
13681                                        .unwrap_or(LiteralValue::Empty);
13682                                    let new = if cell.sheet_id == anchor.sheet_id
13683                                        && cell.coord.row() == anchor.coord.row()
13684                                        && cell.coord.col() == anchor.coord.col()
13685                                    {
13686                                        other.clone()
13687                                    } else {
13688                                        LiteralValue::Empty
13689                                    };
13690                                    Self::record_cell_if_changed(d, cell, &old, &new);
13691                                }
13692                            }
13693                        }
13694                        self.graph.clear_spill_region(vertex_id);
13695                        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
13696                            self.record_structural_change(scope);
13697                        }
13698                        if self.config.arrow_storage_enabled
13699                            && self.config.delta_overlay_enabled
13700                            && self.config.write_formula_overlay_enabled
13701                        {
13702                            let empty = LiteralValue::Empty;
13703                            for cell in spill_cells.iter() {
13704                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
13705                                self.mirror_value_to_computed_overlay(
13706                                    &sheet_name,
13707                                    cell.coord.row() + 1,
13708                                    cell.coord.col() + 1,
13709                                    &empty,
13710                                );
13711                            }
13712                        }
13713                        self.graph.update_vertex_value_ref(vertex_id, &other);
13714                        // Optionally mirror into Arrow overlay for Arrow-backed reads
13715                        if self.config.arrow_storage_enabled
13716                            && self.config.delta_overlay_enabled
13717                            && self.config.write_formula_overlay_enabled
13718                        {
13719                            let anchor = self
13720                                .graph
13721                                .get_cell_ref(vertex_id)
13722                                .expect("cell ref for vertex");
13723                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13724                            self.mirror_value_to_computed_overlay(
13725                                &sheet_name,
13726                                anchor.coord.row() + 1,
13727                                anchor.coord.col() + 1,
13728                                &other,
13729                            );
13730                        }
13731                        Ok(other)
13732                    }
13733                }
13734            }
13735            Err(e) => {
13736                // Runtime Excel error: store as a cell value instead of propagating
13737                // as an exception so bulk eval paths don't fail the whole pass.
13738                let spill_cells = self
13739                    .graph
13740                    .spill_cells_for_anchor(vertex_id)
13741                    .map(|cells| cells.to_vec())
13742                    .unwrap_or_default();
13743                let err_val = LiteralValue::Error(e.clone());
13744                if let Some(d) = delta
13745                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
13746                {
13747                    if spill_cells.is_empty() {
13748                        let old = self
13749                            .read_cell_value(
13750                                self.graph.sheet_name(anchor.sheet_id),
13751                                anchor.coord.row() + 1,
13752                                anchor.coord.col() + 1,
13753                            )
13754                            .unwrap_or(LiteralValue::Empty);
13755                        if old != err_val {
13756                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
13757                        }
13758                    } else {
13759                        for cell in spill_cells.iter() {
13760                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
13761                            let old = self
13762                                .get_cell_value(
13763                                    sheet_name,
13764                                    cell.coord.row() + 1,
13765                                    cell.coord.col() + 1,
13766                                )
13767                                .unwrap_or(LiteralValue::Empty);
13768                            let new = if cell.sheet_id == anchor.sheet_id
13769                                && cell.coord.row() == anchor.coord.row()
13770                                && cell.coord.col() == anchor.coord.col()
13771                            {
13772                                err_val.clone()
13773                            } else {
13774                                LiteralValue::Empty
13775                            };
13776                            Self::record_cell_if_changed(d, cell, &old, &new);
13777                        }
13778                    }
13779                }
13780                self.graph.clear_spill_region(vertex_id);
13781                if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
13782                    self.record_structural_change(scope);
13783                }
13784                if self.config.arrow_storage_enabled
13785                    && self.config.delta_overlay_enabled
13786                    && self.config.write_formula_overlay_enabled
13787                {
13788                    let empty = LiteralValue::Empty;
13789                    for cell in spill_cells.iter() {
13790                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
13791                        self.mirror_value_to_computed_overlay(
13792                            &sheet_name,
13793                            cell.coord.row() + 1,
13794                            cell.coord.col() + 1,
13795                            &empty,
13796                        );
13797                    }
13798                }
13799                self.graph.update_vertex_value_ref(vertex_id, &err_val);
13800                if self.config.arrow_storage_enabled
13801                    && self.config.delta_overlay_enabled
13802                    && self.config.write_formula_overlay_enabled
13803                {
13804                    let anchor = self
13805                        .graph
13806                        .get_cell_ref(vertex_id)
13807                        .expect("cell ref for vertex");
13808                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13809                    self.mirror_value_to_computed_overlay(
13810                        &sheet_name,
13811                        anchor.coord.row() + 1,
13812                        anchor.coord.col() + 1,
13813                        &err_val,
13814                    );
13815                }
13816                Ok(err_val)
13817            }
13818        }
13819    }
13820
13821    fn evaluate_named_scalar(
13822        &mut self,
13823        vertex_id: VertexId,
13824        sheet_id: SheetId,
13825    ) -> Result<LiteralValue, ExcelError> {
13826        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
13827            ExcelError::new(ExcelErrorKind::Name)
13828                .with_message("Named range metadata missing".to_string())
13829        })?;
13830
13831        match &named_range.definition {
13832            NamedDefinition::Cell(cell_ref) => {
13833                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13834                let row = cell_ref.coord.row() + 1;
13835                let col = cell_ref.coord.col() + 1;
13836
13837                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
13838                    && matches!(
13839                        self.graph.get_vertex_kind(dep_vertex),
13840                        VertexKind::FormulaScalar | VertexKind::FormulaArray
13841                    )
13842                {
13843                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
13844                    let value = self.evaluate_vertex(dep_vertex)?;
13845                    self.graph.update_vertex_value_ref(vertex_id, &value);
13846                    Ok(value)
13847                } else {
13848                    let value = self
13849                        .get_cell_value(sheet_name, row, col)
13850                        .unwrap_or(LiteralValue::Empty);
13851                    self.graph.update_vertex_value_ref(vertex_id, &value);
13852                    Ok(value)
13853                }
13854            }
13855            NamedDefinition::Literal(v) => {
13856                let out = v.clone();
13857                self.graph.update_vertex_value_ref(vertex_id, &out);
13858                Ok(out)
13859            }
13860            NamedDefinition::Formula { ast, .. } => {
13861                let context_sheet = match named_range.scope {
13862                    NameScope::Sheet(id) => id,
13863                    NameScope::Workbook => sheet_id,
13864                };
13865                let sheet_name = self.graph.sheet_name(context_sheet);
13866                let cell_ref = self
13867                    .graph
13868                    .get_cell_ref(vertex_id)
13869                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
13870                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13871                match interpreter.evaluate_ast(ast) {
13872                    Ok(cv) => {
13873                        let value = cv.into_literal();
13874                        match value {
13875                            LiteralValue::Array(_) => {
13876                                let err = ExcelError::new(ExcelErrorKind::NImpl)
13877                                    .with_message("Array result in scalar named range".to_string());
13878                                let err_val = LiteralValue::Error(err.clone());
13879                                self.graph.update_vertex_value_ref(vertex_id, &err_val);
13880                                Ok(err_val)
13881                            }
13882                            other => {
13883                                self.graph.update_vertex_value_ref(vertex_id, &other);
13884                                Ok(other)
13885                            }
13886                        }
13887                    }
13888                    Err(err) => {
13889                        let err_val = LiteralValue::Error(err.clone());
13890                        self.graph.update_vertex_value_ref(vertex_id, &err_val);
13891                        Ok(err_val)
13892                    }
13893                }
13894            }
13895            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
13896                .with_message("Range-valued name evaluated as scalar".to_string())),
13897        }
13898    }
13899
13900    fn evaluate_named_array(
13901        &mut self,
13902        vertex_id: VertexId,
13903        sheet_id: SheetId,
13904    ) -> Result<LiteralValue, ExcelError> {
13905        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
13906            ExcelError::new(ExcelErrorKind::Name)
13907                .with_message("Named range metadata missing".to_string())
13908        })?;
13909
13910        let out = match &named_range.definition {
13911            NamedDefinition::Range(range_ref) => {
13912                if range_ref.start.sheet_id != range_ref.end.sheet_id {
13913                    return Err(ExcelError::new(ExcelErrorKind::Ref)
13914                        .with_message("Named range cannot span sheets".to_string()));
13915                }
13916
13917                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
13918                let sr0 = range_ref.start.coord.row();
13919                let sc0 = range_ref.start.coord.col();
13920                let er0 = range_ref.end.coord.row();
13921                let ec0 = range_ref.end.coord.col();
13922                if sr0 > er0 || sc0 > ec0 {
13923                    return Err(ExcelError::new(ExcelErrorKind::Ref)
13924                        .with_message("Invalid named range bounds".to_string()));
13925                }
13926
13927                let h = (er0 - sr0 + 1) as usize;
13928                let w = (ec0 - sc0 + 1) as usize;
13929                let cell_count = (h as u64).saturating_mul(w as u64);
13930                if cell_count > self.config.spill.max_spill_cells as u64 {
13931                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
13932                        "Named range too large to materialize as an array".to_string(),
13933                    ));
13934                }
13935
13936                let mut rows = Vec::with_capacity(h);
13937                for r0 in sr0..=er0 {
13938                    let mut row = Vec::with_capacity(w);
13939                    for c0 in sc0..=ec0 {
13940                        let v = self
13941                            .get_cell_value(sheet_name, r0 + 1, c0 + 1)
13942                            .unwrap_or(LiteralValue::Empty);
13943                        row.push(v);
13944                    }
13945                    rows.push(row);
13946                }
13947                LiteralValue::Array(rows)
13948            }
13949            NamedDefinition::Cell(cell_ref) => {
13950                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13951                let row = cell_ref.coord.row() + 1;
13952                let col = cell_ref.coord.col() + 1;
13953                let v = self
13954                    .get_cell_value(sheet_name, row, col)
13955                    .unwrap_or(LiteralValue::Empty);
13956                LiteralValue::Array(vec![vec![v]])
13957            }
13958            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
13959            NamedDefinition::Formula { ast, .. } => {
13960                let context_sheet = match named_range.scope {
13961                    NameScope::Sheet(id) => id,
13962                    NameScope::Workbook => sheet_id,
13963                };
13964                let sheet_name = self.graph.sheet_name(context_sheet);
13965                let cell_ref = self
13966                    .graph
13967                    .get_cell_ref(vertex_id)
13968                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
13969                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13970                match interpreter.evaluate_ast(ast) {
13971                    Ok(cv) => {
13972                        let v = cv.into_literal();
13973                        match v {
13974                            LiteralValue::Array(_) => v,
13975                            other => LiteralValue::Array(vec![vec![other]]),
13976                        }
13977                    }
13978                    Err(err) => LiteralValue::Error(err),
13979                }
13980            }
13981        };
13982
13983        self.graph.update_vertex_value_ref(vertex_id, &out);
13984        Ok(out)
13985    }
13986
13987    fn replan_exhausted_error(&self, limit: usize, context: &str) -> ExcelError {
13988        crate::engine::ResourceLedgerError::Exhausted(
13989            formualizer_common::ResourceExhaustionDetail {
13990                reason: formualizer_common::ResourceExhaustionReason::WorkUnits,
13991                limit: limit as u64,
13992                observed: limit.saturating_add(1) as u64,
13993                request_id: self
13994                    .active_evaluation_resource_request
13995                    .as_ref()
13996                    .map(|stats| stats.request_id),
13997            },
13998        )
13999        .into_excel_error()
14000        .with_message(format!("{context} did not converge after {limit} replans"))
14001    }
14002
14003    fn transient_target_preparation_stale(error: &ExcelError) -> bool {
14004        matches!(
14005            &error.extra,
14006            formualizer_common::ExcelErrorExtra::PreparationStale {
14007                reason: formualizer_common::PreparationStaleReason::Semantic
14008                    | formualizer_common::PreparationStaleReason::Provider
14009            }
14010        )
14011    }
14012
14013    fn prepare_graph_for_routed_evaluation(
14014        &mut self,
14015        targets: &[crate::engine::EvaluationTarget],
14016        options: &crate::engine::TargetEvalOptions<'_>,
14017    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
14018        const MAX_TRANSIENT_PREPARATION_RETRIES: usize = 2;
14019        let mut retries = 0usize;
14020        loop {
14021            match self.prepare_graph_for_targets_unobserved(targets, options) {
14022                Err(error)
14023                    if Self::transient_target_preparation_stale(&error)
14024                        && retries < MAX_TRANSIENT_PREPARATION_RETRIES =>
14025                {
14026                    retries = retries.saturating_add(1);
14027                }
14028                result => return result,
14029            }
14030        }
14031    }
14032
14033    fn evaluate_mixed_targets(
14034        &mut self,
14035        targets: &[crate::engine::EvaluationTarget],
14036        delta: Option<&mut DeltaCollector>,
14037    ) -> Result<EvalResult, ExcelError> {
14038        let _source_cache = self.source_cache_session();
14039        let cancel = self.active_cancel_flag.clone();
14040        let options = crate::engine::TargetEvalOptions {
14041            request_id: self
14042                .active_evaluation_resource_request
14043                .as_ref()
14044                .map(|stats| stats.request_id),
14045            cancel,
14046            deadline: None,
14047            budgets: None,
14048            opaque_policy: crate::engine::OpaquePreparePolicy::Widen,
14049        };
14050        self.prepare_and_execute_target_recipe(targets, &options, delta)
14051    }
14052
14053    fn prepare_and_execute_target_recipe(
14054        &mut self,
14055        targets: &[crate::engine::EvaluationTarget],
14056        options: &crate::engine::TargetEvalOptions<'_>,
14057        delta: Option<&mut DeltaCollector>,
14058    ) -> Result<EvalResult, ExcelError> {
14059        let preparation = self.prepare_graph_for_routed_evaluation(targets, options)?;
14060        self.execute_prepared_target_recipe(targets, &preparation.widened_scope, delta)
14061    }
14062
14063    fn execute_prepared_target_recipe(
14064        &mut self,
14065        targets: &[crate::engine::EvaluationTarget],
14066        scope: &crate::engine::PrepareScope,
14067        delta: Option<&mut DeltaCollector>,
14068    ) -> Result<EvalResult, ExcelError> {
14069        self.require_unified_authority()?;
14070        if matches!(scope, crate::engine::PrepareScope::Workbook)
14071            && let Some(stats) = self.active_evaluation_resource_request.as_mut()
14072        {
14073            stats.workbook_exact_attempts = stats.workbook_exact_attempts.max(1);
14074        }
14075        let mut roots = self.resolve_target_producers(targets)?;
14076        if let crate::engine::PrepareScope::Sheets(sheets) = scope {
14077            let request_id = self
14078                .active_evaluation_resource_request
14079                .as_ref()
14080                .map(|request| request.request_id);
14081            let root_count = roots.len();
14082            let mut widened_roots =
14083                OrderedTargetProducers::from_ordered(std::mem::take(&mut roots))
14084                    .map_err(|_| target_root_allocation_error(root_count, request_id))?;
14085            let sheet_ids = sheets
14086                .iter()
14087                .filter_map(|sheet| self.graph.sheet_id(sheet))
14088                .collect::<FxHashSet<_>>();
14089            for vertex in self.graph.formula_vertices() {
14090                if sheet_ids.contains(&self.graph.get_vertex_sheet_id(vertex)) {
14091                    widened_roots
14092                        .push(crate::engine::target_preparation::TargetProducer::Legacy(
14093                            vertex,
14094                        ))
14095                        .map_err(|_| {
14096                            target_root_allocation_error(widened_roots.len() + 1, request_id)
14097                        })?;
14098                }
14099            }
14100            roots = widened_roots.into_vec();
14101        }
14102        self.begin_evaluation_request();
14103        #[cfg(any(test, feature = "legacy_oracle"))]
14104        self.graph.flush_pending_edge_deltas();
14105        let workbook_scope = matches!(scope, crate::engine::PrepareScope::Workbook);
14106        if workbook_scope {
14107            if let Some(delta) = delta {
14108                self.evaluate_all_with_delta_collector(delta)
14109            } else {
14110                self.evaluate_all_legacy_impl()
14111            }
14112        } else {
14113            self.evaluate_legacy_target_roots(&roots, delta)
14114        }
14115    }
14116
14117    fn legacy_coordinate_targets(
14118        &mut self,
14119        targets: &[(&str, u32, u32)],
14120    ) -> Vec<crate::engine::EvaluationTarget> {
14121        targets
14122            .iter()
14123            .map(|(sheet, row, col)| {
14124                // Compatibility APIs historically interned an unknown target sheet
14125                // and returned an empty value rather than rejecting the target.
14126                self.graph.sheet_id_mut(sheet);
14127                crate::engine::EvaluationTarget::Cell {
14128                    sheet: (*sheet).to_string(),
14129                    row: *row,
14130                    col: *col,
14131                }
14132            })
14133            .collect()
14134    }
14135
14136    /// Evaluate the necessary mixed producer closure for typed cell, range, name, and table targets.
14137    pub fn evaluate_targets(
14138        &mut self,
14139        targets: &[crate::engine::EvaluationTarget],
14140    ) -> Result<EvalResult, ExcelError> {
14141        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14142            engine.observe_function_semantic_epoch()?;
14143            engine.validate_deterministic_mode()?;
14144            engine.evaluate_mixed_targets(targets, None)
14145        })
14146    }
14147
14148    /// Evaluate typed targets with explicit preparation policy and request controls.
14149    pub fn evaluate_targets_with_options(
14150        &mut self,
14151        targets: &[crate::engine::EvaluationTarget],
14152        options: crate::engine::TargetEvalOptions<'_>,
14153    ) -> Result<EvalResult, ExcelError> {
14154        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14155            engine.active_cancel_flag = options.cancel.clone();
14156            engine.active_evaluation_deadline = options.deadline;
14157            let result = (|| {
14158                engine.cancellation_checkpoint("Evaluation cancelled before target preparation")?;
14159                engine.observe_function_semantic_epoch()?;
14160                engine.validate_deterministic_mode()?;
14161                let _source_cache = engine.source_cache_session();
14162                engine.prepare_and_execute_target_recipe(targets, &options, None)
14163            })();
14164            engine.active_cancel_flag = None;
14165            engine.active_evaluation_deadline = None;
14166            result
14167        })
14168    }
14169
14170    /// Evaluate typed targets and return the versioned run/region delta for the request.
14171    pub fn evaluate_targets_with_delta(
14172        &mut self,
14173        targets: &[crate::engine::EvaluationTarget],
14174    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
14175        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
14176            engine.observe_function_semantic_epoch()?;
14177            engine.validate_deterministic_mode()?;
14178            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14179            let result = engine.evaluate_mixed_targets(targets, Some(&mut collector))?;
14180            Ok((result, collector.finish_target()))
14181        })
14182    }
14183
14184    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
14185    pub fn evaluate_until(
14186        &mut self,
14187        targets: &[(&str, u32, u32)],
14188    ) -> Result<EvalResult, ExcelError> {
14189        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14190            engine.evaluate_until_unobserved(targets)
14191        })
14192    }
14193
14194    fn evaluate_until_unobserved(
14195        &mut self,
14196        targets: &[(&str, u32, u32)],
14197    ) -> Result<EvalResult, ExcelError> {
14198        self.observe_function_semantic_epoch()?;
14199        let targets = self.legacy_coordinate_targets(targets);
14200        self.evaluate_mixed_targets(&targets, None)
14201    }
14202
14203    fn evaluate_until_with_delta_collector(
14204        &mut self,
14205        targets: &[(&str, u32, u32)],
14206        delta: &mut DeltaCollector,
14207    ) -> Result<EvalResult, ExcelError> {
14208        let targets = self.legacy_coordinate_targets(targets);
14209        self.evaluate_mixed_targets(&targets, Some(delta))
14210    }
14211
14212    fn evaluate_legacy_target_roots(
14213        &mut self,
14214        roots: &[crate::engine::target_preparation::TargetProducer],
14215        mut delta: Option<&mut DeltaCollector>,
14216    ) -> Result<EvalResult, ExcelError> {
14217        use crate::engine::target_preparation::TargetProducer;
14218        #[cfg(any(test, feature = "benchmark_internal"))]
14219        {
14220            self.recalc_reuse_probe
14221                .get_mut()
14222                .unwrap()
14223                .legacy_target_requests += 1;
14224        }
14225        let start = crate::instant::FzInstant::now();
14226        let root_vertices = roots
14227            .iter()
14228            .filter_map(|root| match root {
14229                TargetProducer::Legacy(vertex) | TargetProducer::Symbol(vertex) => Some(*vertex),
14230                TargetProducer::ValueOnly(_) => None,
14231            })
14232            .collect::<Vec<_>>();
14233        let mut computed_vertices = 0usize;
14234        let mut cycle_errors = 0usize;
14235        let mut replans = 0usize;
14236        const MAX_REPLAN: usize = 5;
14237        self.graph.authority_sync();
14238        loop {
14239            let (precedents_to_eval, old_vdeps) = self.demand_subgraph(&root_vertices)?;
14240            if precedents_to_eval.is_empty() {
14241                break;
14242            }
14243            #[cfg(any(test, feature = "benchmark_internal"))]
14244            {
14245                self.recalc_reuse_probe
14246                    .get_mut()
14247                    .unwrap()
14248                    .target_schedule_builds += 1;
14249            }
14250            let schedule = {
14251                self.graph.authority_sync();
14252                let mut ledger = self.active_resource_ledger.take();
14253                let result = self.create_authority_schedule(
14254                    &precedents_to_eval,
14255                    &old_vdeps,
14256                    ledger.as_mut(),
14257                );
14258                self.active_resource_ledger = ledger;
14259                result?
14260            };
14261            self.begin_pass(&schedule);
14262            for (unit_index, &unit) in schedule.units.iter().enumerate() {
14263                self.cancellation_checkpoint("Evaluation cancelled before target schedule unit")?;
14264                match unit {
14265                    ScheduleUnit::Cycle(index) => {
14266                        if self.handle_cycle_unit(
14267                            schedule.unit_cycle(index),
14268                            delta.as_deref_mut(),
14269                            None,
14270                            None,
14271                        )? > 0
14272                        {
14273                            cycle_errors = cycle_errors.saturating_add(1);
14274                        }
14275                    }
14276                    ScheduleUnit::Layer(index) => {
14277                        let layer = schedule.unit_layer(index);
14278                        let evaluated = if let Some(delta) = delta.as_deref_mut() {
14279                            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14280                                self.evaluate_layer_parallel_with_delta(layer, delta)?
14281                            } else {
14282                                self.evaluate_layer_sequential_with_delta(layer, delta)?
14283                            }
14284                        } else if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14285                            self.evaluate_layer_parallel(layer)?
14286                        } else {
14287                            self.evaluate_layer_sequential(layer)?
14288                        };
14289                        computed_vertices = computed_vertices.saturating_add(evaluated);
14290                    }
14291                }
14292                if self.stop_after_unit(&schedule, unit_index) {
14293                    break;
14294                }
14295            }
14296            let changed = self.changed_virtual_dep_vertices(&precedents_to_eval, &old_vdeps);
14297            self.resource_checkpoint(0)?;
14298            if !self.finish_target_pass_dirty(&precedents_to_eval, &changed) {
14299                break;
14300            }
14301            if replans >= MAX_REPLAN {
14302                return Err(self.replan_exhausted_error(
14303                    MAX_REPLAN,
14304                    "targeted legacy dynamic dependency evaluation",
14305                ));
14306            }
14307            replans = replans.saturating_add(1);
14308        }
14309        self.redirty_for_next_recalc();
14310        Ok(EvalResult {
14311            computed_vertices,
14312            cycle_errors,
14313            elapsed: start.elapsed(),
14314        })
14315    }
14316
14317    /// Build a revision-bound compatibility plan covering every prepared formula vertex.
14318    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
14319        if self.has_staged_formulas() || self.staged_formula_index.has_packages() {
14320            return Err(
14321                Self::plan_stale(formualizer_common::PlanStaleReason::Staged).with_message(
14322                    "compatibility recalculation plans require all staged formulas to be prepared",
14323                ),
14324            );
14325        }
14326        let key = self.recalc_plan_key();
14327        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
14328        vertices.sort_unstable();
14329        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
14330        let schedule = if vertices.is_empty() {
14331            crate::engine::Schedule {
14332                units: Vec::new(),
14333                layers: Vec::new(),
14334                cycles: Vec::new(),
14335            }
14336        } else {
14337            self.create_evaluation_schedule_uncached(&vertices, None)?.0
14338        };
14339        self.validate_recalc_plan_key(&key)?;
14340        Ok(RecalcPlan {
14341            key,
14342            kind: RecalcPlanKind::CompatibilityFull {
14343                schedule,
14344                has_dynamic_refs,
14345            },
14346        })
14347    }
14348
14349    /// Prepare stable typed targets and retain a revision-bound run-local recipe.
14350    pub fn build_recalc_plan_for_targets(
14351        &mut self,
14352        targets: &[crate::engine::EvaluationTarget],
14353    ) -> Result<RecalcPlan, ExcelError> {
14354        self.build_recalc_plan_for_targets_with_options(
14355            targets,
14356            crate::engine::TargetEvalOptions::default(),
14357        )
14358    }
14359
14360    pub fn build_recalc_plan_for_targets_with_options(
14361        &mut self,
14362        targets: &[crate::engine::EvaluationTarget],
14363        options: crate::engine::TargetEvalOptions<'_>,
14364    ) -> Result<RecalcPlan, ExcelError> {
14365        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14366            engine.observe_function_semantic_epoch()?;
14367            engine.validate_deterministic_mode()?;
14368            let _source_cache = engine.source_cache_session();
14369            let preparation = engine.prepare_graph_for_routed_evaluation(targets, &options)?;
14370            #[cfg(any(test, feature = "legacy_oracle"))]
14371            engine.graph.flush_pending_edge_deltas();
14372            let topology = if matches!(
14373                preparation.widened_scope,
14374                crate::engine::PrepareScope::Workbook
14375            ) {
14376                RecalcTopology::Workbook
14377            } else {
14378                RecalcTopology::RunLocalRecipe
14379            };
14380            Ok(RecalcPlan {
14381                key: engine.recalc_plan_key(),
14382                kind: RecalcPlanKind::Target {
14383                    targets: targets.to_vec(),
14384                    scope: preparation.widened_scope,
14385                    topology,
14386                    dynamic_policy: DynamicPlanPolicy::BoundedTargetReplan,
14387                },
14388            })
14389        })
14390    }
14391
14392    /// Evaluate using a previously constructed compatibility or target plan.
14393    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
14394        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14395            engine.evaluate_recalc_plan_unobserved(plan)
14396        })
14397    }
14398
14399    pub fn evaluate_recalc_plan_with_controls(
14400        &mut self,
14401        plan: &RecalcPlan,
14402        cancel: Option<crate::engine::CancelToken>,
14403        deadline: Option<Instant>,
14404    ) -> Result<EvalResult, ExcelError> {
14405        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14406            engine.active_cancel_flag = cancel.clone();
14407            engine.active_evaluation_deadline = deadline;
14408            let result = engine.evaluate_recalc_plan_unobserved(plan);
14409            engine.active_cancel_flag = None;
14410            engine.active_evaluation_deadline = None;
14411            result
14412        })
14413    }
14414
14415    fn evaluate_recalc_plan_unobserved(
14416        &mut self,
14417        plan: &RecalcPlan,
14418    ) -> Result<EvalResult, ExcelError> {
14419        #[cfg(any(test, feature = "legacy_oracle"))]
14420        self.graph.flush_pending_edge_deltas();
14421        self.validate_recalc_plan_key(&plan.key)?;
14422        self.cancellation_checkpoint("Evaluation cancelled before recalculation plan execution")?;
14423        self.validate_deterministic_mode()?;
14424
14425        match &plan.kind {
14426            RecalcPlanKind::Target {
14427                targets,
14428                scope,
14429                topology,
14430                dynamic_policy,
14431            } => {
14432                debug_assert_eq!(*dynamic_policy, DynamicPlanPolicy::BoundedTargetReplan);
14433                debug_assert_eq!(
14434                    matches!(topology, RecalcTopology::Workbook),
14435                    matches!(scope, crate::engine::PrepareScope::Workbook)
14436                );
14437                let _source_cache = self.source_cache_session();
14438                self.execute_prepared_target_recipe(targets, scope, None)
14439            }
14440            RecalcPlanKind::CompatibilityFull {
14441                schedule,
14442                has_dynamic_refs,
14443            } => {
14444                let _source_cache = self.source_cache_session();
14445                self.begin_evaluation_request();
14446                if *has_dynamic_refs {
14447                    self.virtual_dep_fallback_activations =
14448                        self.virtual_dep_fallback_activations.saturating_add(1);
14449                    return self.evaluate_all_coordinator();
14450                }
14451
14452                let start = crate::instant::FzInstant::now();
14453                let dirty_vertices = self.graph.get_evaluation_vertices();
14454                if dirty_vertices.is_empty() {
14455                    return Ok(EvalResult {
14456                        computed_vertices: 0,
14457                        cycle_errors: 0,
14458                        elapsed: start.elapsed(),
14459                    });
14460                }
14461
14462                let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
14463                let mut computed_vertices = 0;
14464                let mut cycle_errors = 0;
14465                for &unit in &schedule.units {
14466                    self.cancellation_checkpoint(
14467                        "Evaluation cancelled before recalculation plan schedule unit",
14468                    )?;
14469                    match unit {
14470                        ScheduleUnit::Cycle(i) => {
14471                            let stamped = self.handle_cycle_unit(
14472                                schedule.unit_cycle(i),
14473                                None,
14474                                Some(&dirty_set),
14475                                None,
14476                            )?;
14477                            if stamped > 0 {
14478                                cycle_errors += 1;
14479                            }
14480                        }
14481                        ScheduleUnit::Layer(i) => {
14482                            let work: Vec<VertexId> = schedule
14483                                .unit_layer(i)
14484                                .vertices
14485                                .iter()
14486                                .copied()
14487                                .filter(|v| dirty_set.contains(v))
14488                                .collect();
14489                            if work.is_empty() {
14490                                continue;
14491                            }
14492                            let temp_layer = crate::engine::scheduler::Layer::new(work);
14493                            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
14494                                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
14495                            } else {
14496                                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
14497                            }
14498                        }
14499                    }
14500                }
14501
14502                self.resource_checkpoint(0)?;
14503                self.graph.clear_dirty_flags(&dirty_vertices);
14504                self.redirty_for_next_recalc();
14505                Ok(EvalResult {
14506                    computed_vertices,
14507                    cycle_errors,
14508                    elapsed: start.elapsed(),
14509                })
14510            }
14511        }
14512    }
14513}
14514
14515impl<R> Engine<R>
14516where
14517    R: EvaluationContext,
14518{
14519    /// Refuse out-of-scope authority states before evaluation can demote spans
14520    /// or execute a legacy schedule. The public error type is unchanged; NImpl
14521    /// carries the exact internal Unsupported operation for the deferred-scope
14522    /// gate. Admission and allocation failures are not scope exceptions.
14523    fn require_unified_authority(&mut self) -> Result<(), ExcelError> {
14524        self.graph
14525            .authority()
14526            .map(|_| ())
14527            .map_err(Self::authority_excel_error)
14528    }
14529
14530    fn authority_excel_error(error: crate::engine::authority::store::AuthorityError) -> ExcelError {
14531        use crate::engine::authority::store::AuthorityError;
14532        // Some unchanged behavioral tests assert only `error.kind`, hiding
14533        // the operation in their panic. The opt-in gate trace proves which
14534        // typed error was actually returned; it never changes that error.
14535        #[cfg(test)]
14536        if std::env::var_os("FZ_AUTHORITY_DEFERRED_TRACE").is_some() {
14537            eprintln!("M1B_AUTHORITY_ERROR {error:?}");
14538        }
14539        let kind = match error {
14540            AuthorityError::Unsupported { .. } => ExcelErrorKind::NImpl,
14541            _ => ExcelErrorKind::Error,
14542        };
14543        ExcelError::new(kind).with_message(format!("unified_authority: {error:?}"))
14544    }
14545
14546    /// Evaluate all dirty/volatile vertices
14547    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
14548        // `evaluate_all_unobserved` owns the `observe_function_semantic_epoch` guard.
14549        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
14550            engine.evaluate_all_unobserved()
14551        })
14552    }
14553
14554    fn evaluate_all_unobserved(&mut self) -> Result<EvalResult, ExcelError> {
14555        debug_assert!(
14556            !self.graph.deferred_dirty_active(),
14557            "deferred-dirty scope leaked into evaluate_all: a begin_deferred_dirty \
14558             was not balanced by end_deferred_dirty"
14559        );
14560        self.observe_function_semantic_epoch()?;
14561        self.lookup_index_cache.reset_counters();
14562        let _source_cache = self.source_cache_session();
14563        self.validate_deterministic_mode()?;
14564        if self.config.defer_graph_building {
14565            // Build graph for all staged formulas before evaluating
14566            self.build_graph_all()?;
14567        }
14568        self.evaluate_all_coordinator()
14569    }
14570
14571    /// Coordinator for `evaluate_all`: starts the evaluation request and runs
14572    /// the per-cell pass.
14573    fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
14574        self.require_unified_authority()?;
14575        self.begin_evaluation_request();
14576        self.evaluate_all_legacy_impl()
14577    }
14578
14579    /// Walk a schedule's units in condensation order: stamp each cyclic SCC
14580    /// at its position and evaluate each layer (parallel when enabled).
14581    ///
14582    /// Returns `(computed_vertices, cycle_count)` where `cycle_count` is the
14583    /// number of Cycle units walked (the former `schedule.cycles.len()`).
14584    fn legacy_pass_run_units(
14585        &mut self,
14586        schedule: &crate::engine::scheduler::Schedule,
14587    ) -> Result<(usize, usize), ExcelError> {
14588        let mut computed_vertices = 0;
14589        let mut cycle_count = 0;
14590        self.begin_pass(schedule);
14591        for (unit_index, &unit) in schedule.units.iter().enumerate() {
14592            match unit {
14593                ScheduleUnit::Cycle(i) => {
14594                    if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0 {
14595                        cycle_count += 1;
14596                    }
14597                }
14598                ScheduleUnit::Layer(i) => {
14599                    let layer = schedule.unit_layer(i);
14600                    if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14601                        computed_vertices += self.evaluate_layer_parallel(layer)?;
14602                    } else {
14603                        computed_vertices += self.evaluate_layer_sequential(layer)?;
14604                    }
14605                }
14606            }
14607            if self.stop_after_unit(schedule, unit_index) {
14608                break;
14609            }
14610        }
14611        Ok((computed_vertices, cycle_count))
14612    }
14613
14614    /// Per-cell `evaluate_all` body, reached through the coordinator. This is
14615    /// an internal primitive; it must not be invoked directly from public APIs.
14616    ///
14617    /// Does NOT call `begin_evaluation_request` (cycle-telemetry reset +
14618    /// per-recalc clock sample): request begin happens at the public entry
14619    /// points / coordinators, so one request keeps one clock sample.
14620    fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
14621        self.reset_virtual_dep_telemetry_if_disabled();
14622        let _span_eval =
14623            crate::engine::trace::fz_span!(tracing::Level::INFO, "evaluate", "evaluate.legacy");
14624        let start = crate::instant::FzInstant::now();
14625        let mut computed_vertices = 0;
14626        let mut cycle_errors = 0;
14627        let mut replan_iterations = 0;
14628        const MAX_REPLAN: usize = 5;
14629        let mut telemetry = self
14630            .config
14631            .enable_virtual_dep_telemetry
14632            .then(|| self.start_virtual_dep_telemetry());
14633
14634        loop {
14635            let to_evaluate = self.graph.get_evaluation_vertices();
14636            if to_evaluate.is_empty() {
14637                if let Some(t) = telemetry.as_mut()
14638                    && t.bailout_reason.is_none()
14639                {
14640                    t.bailout_reason = Some("no_work");
14641                }
14642                break;
14643            }
14644
14645            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
14646            if let Some(t) = telemetry.as_mut() {
14647                Self::accumulate_schedule_meta(t, &meta);
14648            }
14649
14650            let (pass_computed, pass_cycles) = self.legacy_pass_run_units(&schedule)?;
14651            computed_vertices += pass_computed;
14652            cycle_errors += pass_cycles;
14653
14654            // Check if dynamic dependencies changed
14655            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
14656            if let Some(t) = telemetry.as_mut() {
14657                t.changed_vdeps_total += changed_vertices.len();
14658            }
14659
14660            self.resource_checkpoint(0)?;
14661            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
14662                if let Some(t) = telemetry.as_mut() {
14663                    t.bailout_reason = Some("converged");
14664                }
14665                break;
14666            }
14667            if replan_iterations >= MAX_REPLAN {
14668                if let Some(mut t) = telemetry.take() {
14669                    t.bailout_reason = Some("max_replan");
14670                    t.replan_iterations = replan_iterations;
14671                    self.last_virtual_dep_telemetry = t;
14672                }
14673                return Err(
14674                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
14675                );
14676            }
14677
14678            replan_iterations += 1;
14679        }
14680
14681        if let Some(mut t) = telemetry {
14682            t.replan_iterations = replan_iterations;
14683            self.last_virtual_dep_telemetry = t;
14684        }
14685
14686        // Re-dirty volatile vertices for the next evaluation cycle
14687        self.redirty_for_next_recalc();
14688
14689        // Advance recalc epoch after a full evaluation pass finishes
14690        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
14691
14692        Ok(EvalResult {
14693            computed_vertices,
14694            cycle_errors,
14695            elapsed: start.elapsed(),
14696        })
14697    }
14698
14699    pub fn evaluate_all_with_target_delta(
14700        &mut self,
14701    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
14702        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
14703            engine.observe_function_semantic_epoch()?;
14704            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14705            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
14706            Ok((result, collector.finish_target()))
14707        })
14708    }
14709
14710    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
14711        self.evaluate_all_with_delta_policy(EvalDeltaCompatibilityPolicy::Unlimited)
14712    }
14713
14714    pub fn evaluate_all_with_delta_policy(
14715        &mut self,
14716        policy: EvalDeltaCompatibilityPolicy,
14717    ) -> Result<(EvalResult, EvalDelta), ExcelError> {
14718        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
14719            engine.observe_function_semantic_epoch()?;
14720            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14721            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
14722            Ok((result, collector.finish_with_policy(policy)?))
14723        })
14724    }
14725
14726    fn evaluate_all_with_delta_collector(
14727        &mut self,
14728        delta: &mut DeltaCollector,
14729    ) -> Result<EvalResult, ExcelError> {
14730        let _source_cache = self.source_cache_session();
14731        if self.config.defer_graph_building {
14732            self.build_graph_all()?;
14733        }
14734        self.require_unified_authority()?;
14735        self.begin_evaluation_request();
14736        self.reset_virtual_dep_telemetry_if_disabled();
14737        let _span_eval = crate::engine::trace::fz_span!(
14738            tracing::Level::INFO,
14739            "evaluate",
14740            "evaluate.legacy_delta"
14741        );
14742        let start = crate::instant::FzInstant::now();
14743        let mut computed_vertices = 0;
14744        let mut cycle_errors = 0;
14745
14746        let mut replan_iterations = 0;
14747        const MAX_REPLAN: usize = 5;
14748        let mut telemetry = self
14749            .config
14750            .enable_virtual_dep_telemetry
14751            .then(|| self.start_virtual_dep_telemetry());
14752
14753        loop {
14754            let to_evaluate = self.graph.get_evaluation_vertices();
14755            if to_evaluate.is_empty() {
14756                if let Some(t) = telemetry.as_mut()
14757                    && t.bailout_reason.is_none()
14758                {
14759                    t.bailout_reason = Some("no_work");
14760                }
14761                break;
14762            }
14763
14764            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
14765            if let Some(t) = telemetry.as_mut() {
14766                Self::accumulate_schedule_meta(t, &meta);
14767            }
14768
14769            self.begin_pass(&schedule);
14770            for (unit_index, &unit) in schedule.units.iter().enumerate() {
14771                match unit {
14772                    ScheduleUnit::Cycle(i) => {
14773                        if self.handle_cycle_unit(
14774                            schedule.unit_cycle(i),
14775                            Some(delta),
14776                            None,
14777                            None,
14778                        )? > 0
14779                        {
14780                            cycle_errors += 1;
14781                        }
14782                    }
14783                    ScheduleUnit::Layer(i) => {
14784                        let layer = schedule.unit_layer(i);
14785                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14786                            computed_vertices +=
14787                                self.evaluate_layer_parallel_with_delta(layer, delta)?;
14788                        } else {
14789                            computed_vertices +=
14790                                self.evaluate_layer_sequential_with_delta(layer, delta)?;
14791                        }
14792                    }
14793                }
14794                if self.stop_after_unit(&schedule, unit_index) {
14795                    break;
14796                }
14797            }
14798
14799            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
14800            if let Some(t) = telemetry.as_mut() {
14801                t.changed_vdeps_total += changed_vertices.len();
14802            }
14803            self.resource_checkpoint(0)?;
14804            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
14805                if let Some(t) = telemetry.as_mut() {
14806                    t.bailout_reason = Some("converged");
14807                }
14808                break;
14809            }
14810            if replan_iterations >= MAX_REPLAN {
14811                if let Some(mut t) = telemetry.take() {
14812                    t.bailout_reason = Some("max_replan");
14813                    t.replan_iterations = replan_iterations;
14814                    self.last_virtual_dep_telemetry = t;
14815                }
14816                return Err(
14817                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
14818                );
14819            }
14820            replan_iterations += 1;
14821        }
14822
14823        if let Some(mut t) = telemetry {
14824            t.replan_iterations = replan_iterations;
14825            self.last_virtual_dep_telemetry = t;
14826        }
14827
14828        self.redirty_for_next_recalc();
14829        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
14830
14831        Ok(EvalResult {
14832            computed_vertices,
14833            cycle_errors,
14834            elapsed: start.elapsed(),
14835        })
14836    }
14837
14838    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
14839    ///
14840    /// This will evaluate only the minimal set of dirty / volatile precedents required
14841    /// to bring the target cell up-to-date (as if a user asked for that single value),
14842    /// rather than scheduling a full workbook recalc. If the cell is already clean and
14843    /// non-volatile, no vertices will be recomputed.
14844    ///
14845    /// Returns the (possibly newly computed) value stored for the cell afterwards.
14846    /// Empty cells return None. Errors are surfaced via the Result type.
14847    pub fn evaluate_cell(
14848        &mut self,
14849        sheet: &str,
14850        row: u32,
14851        col: u32,
14852    ) -> Result<Option<LiteralValue>, ExcelError> {
14853        self.observe_evaluation_resource_request(EvaluationRequestKind::Cell, |engine| {
14854            engine.evaluate_cell_unobserved(sheet, row, col)
14855        })
14856    }
14857
14858    fn evaluate_cell_unobserved(
14859        &mut self,
14860        sheet: &str,
14861        row: u32,
14862        col: u32,
14863    ) -> Result<Option<LiteralValue>, ExcelError> {
14864        if row == 0 || col == 0 {
14865            return Err(ExcelError::new(ExcelErrorKind::Ref)
14866                .with_message("Row and column must be >= 1".to_string()));
14867        }
14868
14869        let result = self.evaluate_cells(&[(sheet, row, col)])?;
14870
14871        match result.len() {
14872            0 => Ok(None),
14873            1 => {
14874                let v = result.into_iter().next().unwrap();
14875                Ok(v)
14876            }
14877            _ => unreachable!("evaluate_cells returned unexpected length"),
14878        }
14879    }
14880
14881    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
14882    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
14883    /// is computed once and evaluated, which is typically faster than calling
14884    /// `evaluate_cell` repeatedly for a related set of targets.
14885    ///
14886    /// Returns the resulting values for each requested target in the same order.
14887    pub fn evaluate_cells(
14888        &mut self,
14889        targets: &[(&str, u32, u32)],
14890    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14891        self.observe_evaluation_resource_request(EvaluationRequestKind::Cells, |engine| {
14892            engine.evaluate_cells_unobserved(targets)
14893        })
14894    }
14895
14896    fn evaluate_cells_unobserved(
14897        &mut self,
14898        targets: &[(&str, u32, u32)],
14899    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14900        self.observe_function_semantic_epoch()?;
14901        debug_assert!(
14902            !self.graph.deferred_dirty_active(),
14903            "deferred-dirty scope leaked into evaluate_cells: a begin_deferred_dirty \
14904             was not balanced by end_deferred_dirty"
14905        );
14906        self.validate_deterministic_mode()?;
14907        if targets.is_empty() {
14908            return Ok(Vec::new());
14909        }
14910        let typed_targets = self.legacy_coordinate_targets(targets);
14911        self.evaluate_mixed_targets(&typed_targets, None)?;
14912        Ok(targets
14913            .iter()
14914            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
14915            .collect())
14916    }
14917
14918    pub fn evaluate_cells_cancellable(
14919        &mut self,
14920        targets: &[(&str, u32, u32)],
14921        cancel: crate::engine::CancelToken,
14922    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14923        self.observe_evaluation_resource_request(
14924            EvaluationRequestKind::CellsCancellable,
14925            |engine| {
14926                engine.observe_function_semantic_epoch()?;
14927                engine.active_cancel_flag = Some(cancel.clone());
14928                let res = engine.evaluate_cells_cancellable_impl(targets, cancel.as_flag());
14929                engine.active_cancel_flag = None;
14930                res
14931            },
14932        )
14933    }
14934
14935    fn evaluate_cells_cancellable_impl(
14936        &mut self,
14937        targets: &[(&str, u32, u32)],
14938        cancel_flag: &AtomicBool,
14939    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14940        self.validate_deterministic_mode()?;
14941        if targets.is_empty() {
14942            return Ok(Vec::new());
14943        }
14944        if cancel_flag.load(Ordering::Relaxed) {
14945            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
14946                .with_message("Evaluation cancelled before target preparation"));
14947        }
14948        let typed_targets = self.legacy_coordinate_targets(targets);
14949        self.evaluate_mixed_targets(&typed_targets, None)?;
14950        Ok(targets
14951            .iter()
14952            .map(|(sheet, row, col)| self.get_cell_value(sheet, *row, *col))
14953            .collect())
14954    }
14955
14956    pub fn evaluate_cells_with_target_delta(
14957        &mut self,
14958        targets: &[(&str, u32, u32)],
14959    ) -> Result<(Vec<Option<LiteralValue>>, crate::engine::TargetEvalDelta), ExcelError> {
14960        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
14961            engine.observe_function_semantic_epoch()?;
14962            engine.validate_deterministic_mode()?;
14963            if targets.is_empty() {
14964                return Ok((Vec::new(), crate::engine::TargetEvalDelta::default()));
14965            }
14966            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14967            engine.evaluate_until_with_delta_collector(targets, &mut collector)?;
14968            let values = targets
14969                .iter()
14970                .map(|(sheet, row, col)| engine.get_cell_value(sheet, *row, *col))
14971                .collect();
14972            Ok((values, collector.finish_target()))
14973        })
14974    }
14975
14976    pub fn evaluate_cells_with_delta(
14977        &mut self,
14978        targets: &[(&str, u32, u32)],
14979    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
14980        self.evaluate_cells_with_delta_policy(targets, EvalDeltaCompatibilityPolicy::Unlimited)
14981    }
14982
14983    pub fn evaluate_cells_with_delta_policy(
14984        &mut self,
14985        targets: &[(&str, u32, u32)],
14986        policy: EvalDeltaCompatibilityPolicy,
14987    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
14988        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
14989            engine.evaluate_cells_with_delta_unobserved(targets, policy)
14990        })
14991    }
14992
14993    fn evaluate_cells_with_delta_unobserved(
14994        &mut self,
14995        targets: &[(&str, u32, u32)],
14996        policy: EvalDeltaCompatibilityPolicy,
14997    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
14998        self.observe_function_semantic_epoch()?;
14999        self.validate_deterministic_mode()?;
15000        if targets.is_empty() {
15001            return Ok((Vec::new(), EvalDelta::default()));
15002        }
15003        let mut collector = DeltaCollector::new(DeltaMode::Cells);
15004        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
15005        let values = targets
15006            .iter()
15007            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
15008            .collect();
15009        Ok((values, collector.finish_with_policy(policy)?))
15010    }
15011
15012    /// Get the evaluation plan for target cells without actually evaluating them
15013    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
15014        if targets.is_empty() {
15015            return Ok(EvalPlan {
15016                total_vertices_to_evaluate: 0,
15017                layers: Vec::new(),
15018                cycles_detected: 0,
15019                dirty_count: 0,
15020                volatile_count: 0,
15021                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15022                estimated_parallel_layers: 0,
15023                target_cells: Vec::new(),
15024            });
15025        }
15026        if self.config.defer_graph_building && self.has_staged_formulas() {
15027            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
15028                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
15029            ));
15030        }
15031
15032        // Convert targets to A1 notation for consistency
15033        let addresses: Vec<String> = targets
15034            .iter()
15035            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
15036            .collect();
15037
15038        // Parse target cell addresses
15039        let mut target_addrs = Vec::new();
15040        for (sheet, row, col) in targets {
15041            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
15042                let coord = Coord::from_excel(*row, *col, true, true);
15043                target_addrs.push(CellRef::new(sheet_id, coord));
15044            }
15045        }
15046
15047        // Find vertex IDs for targets
15048        let mut target_vertex_ids = Vec::new();
15049        for addr in &target_addrs {
15050            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
15051                target_vertex_ids.push(vertex_id);
15052            }
15053        }
15054
15055        if target_vertex_ids.is_empty() {
15056            return Ok(EvalPlan {
15057                total_vertices_to_evaluate: 0,
15058                layers: Vec::new(),
15059                cycles_detected: 0,
15060                dirty_count: 0,
15061                volatile_count: 0,
15062                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15063                estimated_parallel_layers: 0,
15064                target_cells: addresses,
15065            });
15066        }
15067
15068        // Build demand subgraph with virtual edges (same as evaluate_until)
15069        let (precedents_to_eval, vdeps) = self.demand_subgraph(&target_vertex_ids)?;
15070
15071        if precedents_to_eval.is_empty() {
15072            return Ok(EvalPlan {
15073                total_vertices_to_evaluate: 0,
15074                layers: Vec::new(),
15075                cycles_detected: 0,
15076                dirty_count: 0,
15077                volatile_count: 0,
15078                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15079                estimated_parallel_layers: 0,
15080                target_cells: addresses,
15081            });
15082        }
15083
15084        // Count dirty and volatile vertices
15085        let mut dirty_count = 0;
15086        let mut volatile_count = 0;
15087        for &vertex_id in &precedents_to_eval {
15088            if self.graph.is_dirty(vertex_id) {
15089                dirty_count += 1;
15090            }
15091            if self.graph.is_volatile(vertex_id) {
15092                volatile_count += 1;
15093            }
15094        }
15095
15096        // Create schedule for the minimal subgraph honoring virtual edges
15097        let schedule = self.create_authority_schedule(&precedents_to_eval, &vdeps, None)?;
15098
15099        // Build layer information
15100        let mut layers = Vec::new();
15101        let mut estimated_parallel_layers = 0;
15102        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
15103
15104        for layer in &schedule.layers {
15105            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
15106            if parallel_eligible {
15107                estimated_parallel_layers += 1;
15108            }
15109
15110            // Get sample cell addresses (up to 5)
15111            let sample_cells: Vec<String> = layer
15112                .vertices
15113                .iter()
15114                .take(5)
15115                .filter_map(|&vertex_id| {
15116                    self.graph
15117                        .get_cell_ref_for_vertex(vertex_id)
15118                        .map(|cell_ref| {
15119                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15120                            format!(
15121                                "{}!{}{}",
15122                                sheet_name,
15123                                Self::col_to_letters(cell_ref.coord.col().saturating_add(1)),
15124                                cell_ref.coord.row() + 1
15125                            )
15126                        })
15127                })
15128                .collect();
15129
15130            layers.push(LayerInfo {
15131                vertex_count: layer.vertices.len(),
15132                parallel_eligible,
15133                sample_cells,
15134            });
15135        }
15136
15137        Ok(EvalPlan {
15138            total_vertices_to_evaluate: precedents_to_eval.len(),
15139            layers,
15140            cycles_detected: schedule.cycles.len(),
15141            dirty_count,
15142            volatile_count,
15143            parallel_enabled,
15144            estimated_parallel_layers,
15145            target_cells: addresses,
15146        })
15147    }
15148    /// Helper to create a schedule, integrating virtual dependencies automatically.
15149    fn create_evaluation_schedule(
15150        &mut self,
15151        to_evaluate: &[VertexId],
15152    ) -> Result<EvaluationScheduleBuildOutput, ExcelError> {
15153        #[cfg(any(test, feature = "benchmark_internal"))]
15154        {
15155            self.recalc_reuse_probe.get_mut().unwrap().schedule_requests += 1;
15156        }
15157        // Fold pending edge deltas once per schedule build so traversal uses
15158        // the zero-allocation CSR slices (#125).
15159        #[cfg(any(test, feature = "legacy_oracle"))]
15160        self.graph.flush_pending_edge_deltas();
15161        // The cache key includes the authority revision: sync first.
15162        self.graph.authority_sync();
15163        if self.can_use_static_schedule_cache(to_evaluate) {
15164            // A recent schedule for the same request becomes the current one.
15165            let revision = self.schedule_cache_authority_revision();
15166            let current = |e: &CachedScheduleEntry| {
15167                e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15168            };
15169            if !self
15170                .cached_static_schedule
15171                .as_ref()
15172                .is_some_and(|c| current(c) && c.candidate_vertices.equals(to_evaluate))
15173                && let Some(i) = self.recent_schedules.iter().position(|e| {
15174                    current(e)
15175                        && e.candidate_vertices.len() == to_evaluate.len()
15176                        && e.candidate_vertices.equals(to_evaluate)
15177                })
15178            {
15179                let hit = self.recent_schedules.remove(i);
15180                if let Some(previous) = self.cached_static_schedule.replace(hit) {
15181                    self.retain_recent_schedule(previous);
15182                }
15183            }
15184            if let Some(cached) = self.cached_static_schedule.as_ref()
15185                && cached.topology_epoch == self.topology_epoch
15186                && cached.authority_revision == self.schedule_cache_authority_revision()
15187                && cached.candidate_vertices.equals(to_evaluate)
15188            {
15189                let meta = ScheduleBuildMeta {
15190                    candidate_vertices: to_evaluate.len(),
15191                    vdeps_vertices: 0,
15192                    vdeps_edges: 0,
15193                    builder_elapsed_ms: 0,
15194                    used_virtual_schedule: false,
15195                    schedule_cache_hit: true,
15196                    schedule_cache_eligible: true,
15197                };
15198                #[cfg(any(test, feature = "benchmark_internal"))]
15199                {
15200                    let mut probe = self.recalc_reuse_probe.lock().unwrap();
15201                    probe.schedule_cache_hits += 1;
15202                    probe.schedule_shared_handles += 1;
15203                }
15204                return Ok((
15205                    EvaluationSchedule::Shared(Arc::clone(&cached.schedule)),
15206                    FxHashMap::default(),
15207                    meta,
15208                ));
15209            }
15210
15211            let (schedule, vdeps, mut meta) = match self.schedule_from_base(to_evaluate)? {
15212                Some(schedule) => (
15213                    schedule,
15214                    FxHashMap::default(),
15215                    ScheduleBuildMeta {
15216                        candidate_vertices: to_evaluate.len(),
15217                        vdeps_vertices: 0,
15218                        vdeps_edges: 0,
15219                        builder_elapsed_ms: 0,
15220                        used_virtual_schedule: false,
15221                        schedule_cache_hit: false,
15222                        schedule_cache_eligible: true,
15223                    },
15224                ),
15225                None => self.create_evaluation_schedule_active(to_evaluate)?,
15226            };
15227            meta.schedule_cache_hit = false;
15228            meta.schedule_cache_eligible = true;
15229            #[cfg(any(test, feature = "benchmark_internal"))]
15230            {
15231                self.recalc_reuse_probe
15232                    .get_mut()
15233                    .unwrap()
15234                    .schedule_cache_misses += 1;
15235            }
15236            let schedule = if vdeps.is_empty() {
15237                // Clone previously discarded builder spare capacity. Keep that compact
15238                // retained payload while sharing it with the current request.
15239                let mut schedule = schedule;
15240                schedule.units.shrink_to_fit();
15241                for layer in &mut schedule.layers {
15242                    layer.vertices.shrink_to_fit();
15243                }
15244                schedule.layers.shrink_to_fit();
15245                for cycle in &mut schedule.cycles {
15246                    cycle.shrink_to_fit();
15247                }
15248                schedule.cycles.shrink_to_fit();
15249                let schedule = Arc::new(schedule);
15250                #[cfg(any(test, feature = "benchmark_internal"))]
15251                {
15252                    self.recalc_reuse_probe
15253                        .get_mut()
15254                        .unwrap()
15255                        .schedule_shared_handles += 1;
15256                }
15257                let entry = CachedScheduleEntry {
15258                    topology_epoch: self.topology_epoch,
15259                    authority_revision: self.schedule_cache_authority_revision(),
15260                    candidate_vertices: VertexIdRuns::from_slice(to_evaluate),
15261                    schedule: Arc::clone(&schedule),
15262                };
15263                if let Some(previous) = self.cached_static_schedule.replace(entry) {
15264                    self.retain_recent_schedule(previous);
15265                }
15266                EvaluationSchedule::Shared(schedule)
15267            } else {
15268                EvaluationSchedule::Owned(schedule)
15269            };
15270            return Ok((schedule, vdeps, meta));
15271        }
15272
15273        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_active(to_evaluate)?;
15274        meta.schedule_cache_hit = false;
15275        meta.schedule_cache_eligible = false;
15276        #[cfg(any(test, feature = "benchmark_internal"))]
15277        {
15278            self.recalc_reuse_probe
15279                .get_mut()
15280                .unwrap()
15281                .schedule_cache_ineligible += 1;
15282        }
15283        Ok((EvaluationSchedule::Owned(schedule), vdeps, meta))
15284    }
15285
15286    /// Plan reuse: the base schedule restricted to `to_evaluate` when it
15287    /// is current, covers the request, has at least
15288    /// `BASE_SCHEDULE_MIN_REQUEST` candidates, and is at most
15289    /// `BASE_SCHEDULE_RATIO` times its size (restricting walks the whole
15290    /// base; planning costs far more per candidate).
15291    fn schedule_from_base(
15292        &mut self,
15293        to_evaluate: &[VertexId],
15294    ) -> Result<Option<crate::engine::scheduler::Schedule>, ExcelError> {
15295        let revision = self.schedule_cache_authority_revision();
15296        let current = |e: &&CachedScheduleEntry| {
15297            e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15298        };
15299        // The base, or the current schedule when larger (the first
15300        // evaluation's, before a later request replaces it).
15301        let Some(base) = [
15302            self.base_schedule.as_ref(),
15303            self.cached_static_schedule.as_ref(),
15304        ]
15305        .into_iter()
15306        .flatten()
15307        .filter(current)
15308        .max_by_key(|e| e.candidate_vertices.len()) else {
15309            return Ok(None);
15310        };
15311        let base_len = base.candidate_vertices.len();
15312        if base_len <= BASE_SCHEDULE_MIN_VERTICES
15313            || to_evaluate.len() < BASE_SCHEDULE_MIN_REQUEST
15314            || to_evaluate.len() > base_len
15315            || base_len > to_evaluate.len().saturating_mul(BASE_SCHEDULE_RATIO)
15316        {
15317            return Ok(None);
15318        }
15319        let mut keep = crate::engine::idset::DenseIdSet::default();
15320        keep.extend(to_evaluate.iter().copied());
15321        let Some((schedule, kept)) = base.schedule.restrict(&keep) else {
15322            return Ok(None);
15323        };
15324        // Every requested vertex must be in the base.
15325        if kept != keep.len() {
15326            return Ok(None);
15327        }
15328        if let Some(ledger) = self.active_resource_ledger.as_mut() {
15329            let layers: usize = schedule
15330                .layers
15331                .iter()
15332                .map(|l| {
15333                    l.vertices.capacity() * std::mem::size_of::<VertexId>()
15334                        + l.runs.capacity()
15335                            * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
15336                })
15337                .sum();
15338            let bytes = (keep.heap_bytes()
15339                + layers
15340                + schedule.layers.capacity()
15341                    * std::mem::size_of::<crate::engine::scheduler::Layer>()
15342                + schedule.units.capacity()
15343                    * std::mem::size_of::<crate::engine::scheduler::ScheduleUnit>()
15344                + schedule
15345                    .cycles
15346                    .iter()
15347                    .map(|c| c.capacity() * 4)
15348                    .sum::<usize>()) as u64;
15349            ledger
15350                .reserve_schedule_discovery(bytes)
15351                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15352            ledger
15353                .release_scratch(bytes)
15354                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15355        }
15356        #[cfg(debug_assertions)]
15357        self.debug_check_restricted_schedule(to_evaluate, &schedule);
15358        #[cfg(any(test, feature = "benchmark_internal"))]
15359        {
15360            self.recalc_reuse_probe
15361                .get_mut()
15362                .unwrap()
15363                .schedule_base_restrictions += 1;
15364        }
15365        Ok(Some(schedule))
15366    }
15367
15368    /// Debug builds: a restricted schedule holds each requested vertex
15369    /// once and orders the request like a freshly planned schedule (every
15370    /// dependency among the request in an earlier unit).
15371    #[cfg(debug_assertions)]
15372    fn debug_check_restricted_schedule(
15373        &self,
15374        to_evaluate: &[VertexId],
15375        schedule: &crate::engine::scheduler::Schedule,
15376    ) {
15377        let mut position: FxHashMap<VertexId, usize> = FxHashMap::default();
15378        for (u, unit) in schedule.units.iter().enumerate() {
15379            let vertices: &[VertexId] = match *unit {
15380                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
15381                    &schedule.unit_layer(i).vertices
15382                }
15383                crate::engine::scheduler::ScheduleUnit::Cycle(i) => schedule.unit_cycle(i),
15384            };
15385            for &v in vertices {
15386                assert!(
15387                    position.insert(v, u).is_none(),
15388                    "vertex twice in a restricted schedule"
15389                );
15390            }
15391        }
15392        assert_eq!(position.len(), to_evaluate.len());
15393        if to_evaluate.len() > 512 {
15394            return;
15395        }
15396        // Every arc among the request (the store's edge images) goes to an
15397        // earlier unit, or within one sequential layer (a chain) or cycle.
15398        let Ok(store) = self.graph.authority_plan_store() else {
15399            return;
15400        };
15401        let cells: Vec<(VertexId, (u16, u32, u32))> = to_evaluate
15402            .iter()
15403            .filter_map(|&v| self.graph.authority_cell_of_vertex(v).map(|c| (v, c)))
15404            .collect();
15405        for &(v, (sheet, row, col)) in &cells {
15406            let Some(owner) = store.owner_at((sheet, row, col)) else {
15407                continue;
15408            };
15409            let Ok(refined) = store.refine_owner_column(owner, col, row, row, None) else {
15410                continue;
15411            };
15412            for piece in &refined.pieces {
15413                for edge in &refined.edges[piece.edge_start..piece.edge_end] {
15414                    let Some(image) = edge.proj.forward(&piece.domain) else {
15415                        continue;
15416                    };
15417                    for &(d, (s2, r2, c2)) in &cells {
15418                        if d == v
15419                            || edge.proj.sheet != s2
15420                            || !(image.r0 <= r2
15421                                && r2 <= image.r1
15422                                && image.c0 <= c2
15423                                && c2 <= image.c1)
15424                        {
15425                            continue;
15426                        }
15427                        let (pd, pv) = (position[&d], position[&v]);
15428                        let same_ok = pd == pv
15429                            && match schedule.units[pv] {
15430                                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
15431                                    schedule.unit_layer(i).sequential
15432                                }
15433                                crate::engine::scheduler::ScheduleUnit::Cycle(_) => true,
15434                            };
15435                        assert!(
15436                            pd < pv || same_ok,
15437                            "restricted schedule orders {v:?} before its precedent {d:?}"
15438                        );
15439                    }
15440                }
15441            }
15442        }
15443    }
15444
15445    /// Compress family formulas once per authority build (see
15446    /// `EvalConfig::formula_compression`), when no staged or deferred
15447    /// formula package can hold arena ids.
15448    fn maybe_compress_formulas(&mut self) {
15449        if !self.config.formula_compression {
15450            return;
15451        }
15452        let builds = self.graph.authority_host().builds;
15453        if self.compressed_at_build == Some(builds) {
15454            return;
15455        }
15456        self.compressed_at_build = Some(builds);
15457        if self.has_staged_formulas() {
15458            // Staged packages hold arena ids: no compaction. Members that
15459            // are already compressed can still leave the per-cell maps.
15460            self.graph.virtualize_family_members();
15461            return;
15462        }
15463        let pool = self.thread_pool.clone();
15464        let (_, garbage) = self.graph.compress_family_formulas(pool.as_deref());
15465        self.graph.virtualize_family_members();
15466        // Freeing the dropped members' reference texts (one allocation each)
15467        // is most of compaction; with a pool it happens off the critical
15468        // path.
15469        match pool {
15470            Some(pool) if garbage.len() >= 1024 => pool.spawn(move || drop(garbage)),
15471            _ => drop(garbage),
15472        }
15473    }
15474
15475    fn create_evaluation_schedule_active(
15476        &mut self,
15477        to_evaluate: &[VertexId],
15478    ) -> Result<ScheduleBuildOutput, ExcelError> {
15479        self.graph.authority_sync();
15480        self.maybe_compress_formulas();
15481        let mut ledger = self.active_resource_ledger.take();
15482        let result = self.create_evaluation_schedule_uncached(to_evaluate, ledger.as_mut());
15483        self.active_resource_ledger = ledger;
15484        result
15485    }
15486
15487    fn create_evaluation_schedule_uncached(
15488        &self,
15489        to_evaluate: &[VertexId],
15490        #[allow(unused_variables)] ledger: Option<&mut ResourceLedger>,
15491    ) -> Result<ScheduleBuildOutput, ExcelError> {
15492        #[cfg(any(test, feature = "benchmark_internal"))]
15493        {
15494            self.recalc_reuse_probe.lock().unwrap().schedule_builds += 1;
15495        }
15496        let builder = VirtualDepBuilder::new(self);
15497        #[allow(unused_mut)]
15498        let (mut vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
15499            if self.config.enable_virtual_dep_telemetry {
15500                let build_started = crate::instant::FzInstant::now();
15501                let (vdeps, augmented) = builder.build(to_evaluate);
15502                let builder_elapsed_ms = build_started.elapsed().as_millis();
15503                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
15504                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
15505            } else {
15506                let (vdeps, augmented) = builder.build(to_evaluate);
15507                (vdeps, augmented, 0, 0)
15508            };
15509
15510        // Replan hints from stale dynamic reads earlier in this request.
15511        {
15512            self.freshness_merge_hints(to_evaluate, &mut vdeps);
15513            self.freshness_extent_hints(to_evaluate, &mut vdeps);
15514        }
15515        let mut final_evaluate = to_evaluate.to_vec();
15516        if !augmented.is_empty() {
15517            final_evaluate.extend(augmented);
15518            final_evaluate.sort_unstable();
15519            final_evaluate.dedup();
15520        }
15521
15522        let use_virtual = !vdeps.is_empty();
15523
15524        let schedule = self.create_authority_schedule(&final_evaluate, &vdeps, ledger)?;
15525
15526        let meta = ScheduleBuildMeta {
15527            candidate_vertices: to_evaluate.len(),
15528            vdeps_vertices: vdeps.len(),
15529            vdeps_edges,
15530            builder_elapsed_ms,
15531            used_virtual_schedule: use_virtual,
15532            schedule_cache_hit: false,
15533            schedule_cache_eligible: false,
15534        };
15535
15536        Ok((schedule, vdeps, meta))
15537    }
15538
15539    fn create_authority_schedule(
15540        &self,
15541        candidates: &[VertexId],
15542        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
15543        mut ledger: Option<&mut ResourceLedger>,
15544    ) -> Result<crate::engine::scheduler::Schedule, ExcelError> {
15545        use crate::engine::authority::{
15546            geom::{Cover, Rect},
15547            plan_schedule, planner,
15548            proj::{AxisMap, RefProj},
15549            store::{EdgeKey, Tag},
15550        };
15551        let failure = |message: String| {
15552            ExcelError::new(ExcelErrorKind::Error)
15553                .with_message(format!("unified_authority planner: {message}"))
15554        };
15555        self.cancellation_checkpoint("Evaluation cancelled before authority planning")?;
15556        let store = self
15557            .graph
15558            .authority_plan_store()
15559            .map_err(Self::authority_excel_error)?;
15560        // One formula cell without hints (a tiny edit): its plan is the cell
15561        // alone unless it reads itself (`planner::plan_single`).
15562        if let [only] = candidates
15563            && vdeps.is_empty()
15564            && self.graph.authority_host().observed(*only).is_none()
15565            && let Some(cell) = self.graph.authority_cell_of_vertex(*only)
15566            && cell.0 != crate::engine::authority::geom::SYMBOL_SHEET
15567            && let Some(single) = planner::plan_single(store, cell)
15568        {
15569            #[cfg(debug_assertions)]
15570            {
15571                let mut cover = Cover::new();
15572                cover.insert_rect(cell.0, &Rect::new(cell.1, cell.2, cell.1, cell.2));
15573                let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
15574                    .expect("general plan of one cell");
15575                assert_eq!(
15576                    general.cells.as_slice(),
15577                    &[single],
15578                    "single-cell plan differs from the planner at {cell:?}"
15579                );
15580            }
15581            let adapted = plan_schedule::schedule(
15582                &[single],
15583                0,
15584                None,
15585                |cell| {
15586                    self.graph
15587                        .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15588                        .ok_or_else(|| failure("missing executor identity".to_owned()))
15589                },
15590                |_work| Ok(()),
15591            )
15592            .map_err(|error| match error {
15593                plan_schedule::ScheduleError::Runtime(error) => error,
15594                other => failure(format!("{other:?}")),
15595            })?;
15596            if let Some(ledger) = ledger {
15597                ledger
15598                    .reserve_schedule_discovery(adapted.peak_heap_bytes)
15599                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15600                ledger
15601                    .release_scratch(adapted.peak_heap_bytes)
15602                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15603            }
15604            return Ok(adapted.schedule);
15605        }
15606        // A small request of grid formula cells without hints: per-cell
15607        // arcs and longest-path levels (`planner::plan_small`).
15608        if (2..=planner::SMALL_PLAN_MAX).contains(&candidates.len())
15609            && vdeps.is_empty()
15610            && candidates
15611                .iter()
15612                .all(|&v| self.graph.authority_host().observed(v).is_none())
15613        {
15614            let cells: Option<Vec<(u16, u32, u32)>> = candidates
15615                .iter()
15616                .map(|&v| self.graph.authority_cell_of_vertex(v))
15617                .collect();
15618            if let Some(small) = cells.as_deref().and_then(|c| planner::plan_small(store, c)) {
15619                #[cfg(debug_assertions)]
15620                {
15621                    let mut cover = Cover::new();
15622                    for c in cells.as_deref().unwrap_or_default() {
15623                        cover.insert_rect(c.0, &Rect::new(c.1, c.2, c.1, c.2));
15624                    }
15625                    let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
15626                        .expect("general plan of a small request");
15627                    let key = |c: &planner::OrderedCell| (c.sheet, c.row, c.col, c.id, c.owner);
15628                    let mut a: Vec<_> = general.cells.iter().map(key).collect();
15629                    let mut b: Vec<_> = small.iter().map(key).collect();
15630                    a.sort_unstable();
15631                    b.sort_unstable();
15632                    assert_eq!(a, b, "small plan cells differ from the planner");
15633                    assert!(
15634                        general.cells.iter().all(|c| c.cycle.is_none()),
15635                        "small plan of a cyclic request"
15636                    );
15637                }
15638                let adapted = plan_schedule::schedule(
15639                    &small,
15640                    0,
15641                    None,
15642                    |cell| {
15643                        self.graph
15644                            .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15645                            .ok_or_else(|| failure("missing executor identity".to_owned()))
15646                    },
15647                    |_work| Ok(()),
15648                )
15649                .map_err(|error| match error {
15650                    plan_schedule::ScheduleError::Runtime(error) => error,
15651                    other => failure(format!("{other:?}")),
15652                })?;
15653                if let Some(ledger) = ledger {
15654                    ledger
15655                        .reserve_schedule_discovery(adapted.peak_heap_bytes)
15656                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15657                    ledger
15658                        .release_scratch(adapted.peak_heap_bytes)
15659                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15660                }
15661                return Ok(adapted.schedule);
15662            }
15663        }
15664        // Names are symbol-plane nodes (design §4.1): a name vertex plans as
15665        // the unit at its node, between its precedents and its readers.
15666        // Candidates become cells, sorted by (sheet, column, row) and
15667        // coalesced into row intervals: one cover insert per interval.
15668        let mut cover = Cover::new();
15669        {
15670            let cell_of = |&id: &VertexId| {
15671                self.graph
15672                    .authority_cell_of_vertex(id)
15673                    .map(|(sheet, row, col)| (sheet, col, row))
15674            };
15675            // A full recalc maps and sorts every formula: on the pool when
15676            // there is one (first eval's schedule is serial work otherwise).
15677            let cells: Vec<(u16, u32, u32)> = match self.thread_pool.as_deref() {
15678                Some(pool) if candidates.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
15679                    use rayon::prelude::*;
15680                    pool.install(|| {
15681                        let mut cells: Vec<_> = candidates.par_iter().filter_map(cell_of).collect();
15682                        cells.par_sort_unstable();
15683                        cells
15684                    })
15685                }
15686                _ => {
15687                    let mut cells: Vec<_> = candidates.iter().filter_map(cell_of).collect();
15688                    cells.sort_unstable();
15689                    cells
15690                }
15691            };
15692            let mut i = 0;
15693            while i < cells.len() {
15694                let (sheet, col, r0) = cells[i];
15695                let mut r1 = r0;
15696                let mut j = i + 1;
15697                while j < cells.len() && cells[j].0 == sheet && cells[j].1 == col {
15698                    if cells[j].2 > r1 + 1 {
15699                        break;
15700                    }
15701                    r1 = r1.max(cells[j].2);
15702                    j += 1;
15703                }
15704                cover.insert_rect(sheet, &Rect::new(r0, col, r1, col));
15705                i = j;
15706            }
15707        }
15708        let mut hints = Vec::new();
15709        for (&reader, deps) in vdeps {
15710            let Some(reader) = self.graph.authority_cell_of_vertex(reader) else {
15711                continue;
15712            };
15713            for &dependency in deps {
15714                let Some(dep) = self.graph.authority_cell_of_vertex(dependency) else {
15715                    continue;
15716                };
15717                hints.push(planner::PlanHint {
15718                    reader: (reader.0, reader.2, reader.1),
15719                    edge: EdgeKey {
15720                        dep_sheet: reader.0,
15721                        tag: Tag::X,
15722                        lk: u32::MAX,
15723                        proj: RefProj {
15724                            sheet: dep.0,
15725                            rows: AxisMap::fixed(dep.1, dep.1),
15726                            cols: AxisMap::fixed(dep.2, dep.2),
15727                        },
15728                    },
15729                });
15730            }
15731        }
15732        // rdi_dyn: order each dynamic reader after its observed reads.
15733        let host = self.graph.authority_host();
15734        for &id in candidates.iter().filter(|_| host.has_observed()) {
15735            let Some(reads) = host.observed(id) else {
15736                continue;
15737            };
15738            let Some(reader) = self.graph.authority_cell_of_vertex(id) else {
15739                continue;
15740            };
15741            for &(sheet, r0, c0, r1, c1) in reads {
15742                hints.push(planner::PlanHint {
15743                    reader: (reader.0, reader.2, reader.1),
15744                    edge: EdgeKey {
15745                        dep_sheet: reader.0,
15746                        tag: Tag::X,
15747                        lk: u32::MAX,
15748                        proj: RefProj {
15749                            sheet,
15750                            rows: AxisMap::fixed(r0, r1),
15751                            cols: AxisMap::fixed(c0, c1),
15752                        },
15753                    },
15754                });
15755            }
15756        }
15757        hints.sort_unstable_by_key(|hint| hint.reader);
15758        let checkpoint = ledger
15759            .as_ref()
15760            .map_or(0, |ledger| ledger.scratch_checkpoint());
15761        let scratch_limit = ledger
15762            .as_ref()
15763            .and_then(|ledger| ledger.schedule_discovery_limit())
15764            .map(|limit| limit.saturating_sub(checkpoint));
15765        let ordered = planner::plan_with_hints(store, &cover, &hints, scratch_limit, None, None)
15766            .map_err(|error| failure(format!("{error:?}")))?;
15767        // max_work_units is an execution budget. Planning work must be capped
15768        // independently: charging it here changes the observable publication
15769        // boundary (e.g. a spill must commit before the next execution fails).
15770        let adapted = plan_schedule::schedule(
15771            &ordered.cells,
15772            ordered.heap_bytes(),
15773            scratch_limit,
15774            |cell| {
15775                self.graph
15776                    .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15777                    .ok_or_else(|| failure("missing executor identity".to_owned()))
15778            },
15779            |_work| {
15780                self.cancellation_checkpoint("Evaluation cancelled during authority planning")?;
15781                if let Some(ledger) = ledger.as_deref_mut() {
15782                    ledger
15783                        .checkpoint_deadline()
15784                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15785                }
15786                Ok(())
15787            },
15788        )
15789        .map_err(|error| match error {
15790            plan_schedule::ScheduleError::Runtime(error) => error,
15791            other => failure(format!("{other:?}")),
15792        })?;
15793        if let Some(ledger) = ledger {
15794            let peak = ordered.peak_heap_bytes.max(adapted.peak_heap_bytes);
15795            ledger
15796                .reserve_schedule_discovery(peak)
15797                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15798            ledger
15799                .release_scratch(peak)
15800                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15801        }
15802        // The planner's order is scratch now; a full recalc's is large.
15803        match self.thread_pool.as_deref() {
15804            Some(pool) if ordered.cells.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
15805                pool.spawn(move || drop(ordered))
15806            }
15807            _ => drop(ordered),
15808        }
15809        Ok(adapted.schedule)
15810    }
15811
15812    /// Static-schedule cache eligibility. Legacy excludes range readers:
15813    /// their order comes from per-request range virtual deps. Under the
15814    /// authority a range read is a static edge of the relation, so only
15815    /// dynamic readers (whose hints are per request) are excluded, and the
15816    /// key adds the authority revision (design §8.4).
15817    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
15818        {
15819            // A dynamic reader is planned from its observed reads, which the
15820            // key covers (rev.dyn); one without them needs a pre-probe, and
15821            // request-scoped replan hints are never cached.
15822            let host = self.graph.authority_host();
15823            !to_evaluate.is_empty()
15824                && !self.freshness_has_hints()
15825                && to_evaluate
15826                    .iter()
15827                    .all(|&v| !self.graph.is_dynamic(v) || host.observed(v).is_some())
15828        }
15829    }
15830
15831    fn schedule_cache_authority_revision(&self) -> (u64, u64) {
15832        {
15833            // rev.topology is `topology_epoch`; a symbol revision rebuilds the
15834            // store, so it is part of `revision`.
15835            let host = self.graph.authority_host();
15836            (host.revision(), host.rev_dyn())
15837        }
15838    }
15839
15840    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
15841        VirtualDepTelemetry {
15842            fallback_mode_activations: self.virtual_dep_fallback_activations,
15843            ..VirtualDepTelemetry::default()
15844        }
15845    }
15846
15847    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
15848        telemetry.candidate_vertices_total += meta.candidate_vertices;
15849        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
15850        telemetry.vdeps_edges_total += meta.vdeps_edges;
15851        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
15852        if meta.schedule_cache_eligible {
15853            if meta.schedule_cache_hit {
15854                telemetry.schedule_cache_hits += 1;
15855                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
15856            } else {
15857                telemetry.schedule_cache_misses += 1;
15858            }
15859        }
15860        if meta.used_virtual_schedule {
15861            telemetry.schedule_virtual_passes += 1;
15862        } else {
15863            telemetry.schedule_static_passes += 1;
15864        }
15865    }
15866
15867    /// End-of-pass dirty bookkeeping; true when the loop must replan.
15868    /// Legacy: clear the pass, re-dirty readers whose pre-probe changed.
15869    /// Under the authority an armed pass keeps stale and unreached vertices
15870    /// dirty instead (design §8.2, `freshness.rs`).
15871    fn finish_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
15872        self.finish_pass_dirty_scoped(to_evaluate, changed, true)
15873    }
15874
15875    /// [`Self::finish_pass_dirty`] for a targeted pass: only its candidates
15876    /// decide whether to replan.
15877    fn finish_target_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
15878        self.finish_pass_dirty_scoped(to_evaluate, changed, false)
15879    }
15880
15881    fn finish_pass_dirty_scoped(
15882        &mut self,
15883        to_evaluate: &[VertexId],
15884        changed: &[VertexId],
15885        #[allow(unused_variables)] whole_workbook: bool,
15886    ) -> bool {
15887        self.freshness_finish_pass(to_evaluate, changed, whole_workbook)
15888    }
15889
15890    /// Start a pass over `schedule` (arms the freshness recorder).
15891    fn begin_pass(
15892        &mut self,
15893        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
15894    ) {
15895        self.freshness_begin_pass(schedule);
15896    }
15897
15898    /// FR4 layer barrier after unit `index`: true stops the pass.
15899    fn stop_after_unit(
15900        &mut self,
15901        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
15902        #[allow(unused_variables)] index: usize,
15903    ) -> bool {
15904        self.freshness_stop_after_unit(schedule, index)
15905    }
15906
15907    fn changed_virtual_dep_vertices(
15908        &mut self,
15909        to_evaluate: &[VertexId],
15910        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
15911    ) -> Vec<VertexId> {
15912        #[cfg(test)]
15913        if self.force_virtual_dep_changes_remaining_for_test > 0
15914            && let Some(vertex) = to_evaluate.first().copied()
15915        {
15916            self.force_virtual_dep_changes_remaining_for_test -= 1;
15917            return vec![vertex];
15918        }
15919        // An armed pass detects stale dynamic reads directly; the pre-probe
15920        // comparison is legacy's substitute for that (design §8.2).
15921        if self.freshness_armed() {
15922            return Vec::new();
15923        }
15924        if !to_evaluate
15925            .iter()
15926            .copied()
15927            .any(|v| self.graph.is_dynamic(v))
15928        {
15929            return Vec::new();
15930        }
15931
15932        let builder = VirtualDepBuilder::new(self);
15933        let (new_vdeps, _) = builder.build(to_evaluate);
15934
15935        let mut candidates = FxHashSet::default();
15936        candidates.extend(old_vdeps.keys().copied());
15937        candidates.extend(new_vdeps.keys().copied());
15938
15939        let mut changed = Vec::new();
15940        for v in candidates {
15941            if old_vdeps.get(&v) != new_vdeps.get(&v) {
15942                changed.push(v);
15943            }
15944        }
15945        changed
15946    }
15947
15948    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
15949    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
15950    /// Demand candidates of `targets` and their dynamic plan hints: under
15951    /// `unified_authority` from the authority's relation (design §8.3),
15952    /// otherwise from the legacy graph.
15953    #[allow(clippy::type_complexity)]
15954    fn demand_subgraph(
15955        &self,
15956        targets: &[VertexId],
15957    ) -> Result<
15958        (
15959            Vec<VertexId>,
15960            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
15961        ),
15962        ExcelError,
15963    > {
15964        self.authority_demand_subgraph(targets)
15965    }
15966
15967    /// Design §8.3: traverse precedents from the targets over the
15968    /// authority's static relation (symbol nodes are ordinary pieces) plus
15969    /// the dynamic readers' virtual dependencies, and collect what legacy's
15970    /// demand walk collects: dirty or volatile formula cells and every name
15971    /// passed through. No legacy dependency structure is read.
15972    #[allow(clippy::type_complexity)]
15973    fn authority_demand_subgraph(
15974        &self,
15975        targets: &[VertexId],
15976    ) -> Result<
15977        (
15978            Vec<VertexId>,
15979            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
15980        ),
15981        ExcelError,
15982    > {
15983        use crate::engine::authority::geom::{Cell, Rect, SYMBOL_SHEET};
15984        use crate::engine::authority::store::TagFilter;
15985        use rustc_hash::{FxHashMap, FxHashSet};
15986        let store = self
15987            .graph
15988            .authority_plan_store()
15989            .map_err(Self::authority_excel_error)?;
15990        let ids = store.ids();
15991        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
15992        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
15993        let mut visited: FxHashSet<Cell> = FxHashSet::default();
15994        // (cell, authority id or NO_VID): the id lets the side array
15995        // translate the cell without a hash lookup.
15996        let mut stack: Vec<(Cell, u32)> = Vec::new();
15997        // Formula cells under `rect` on `sheet`: identity runs per column.
15998        let push_formulas = |stack: &mut Vec<(Cell, u32)>,
15999                             visited: &FxHashSet<Cell>,
16000                             sheet: u16,
16001                             rect: Rect| {
16002            for col in rect.c0..=rect.c1 {
16003                ids.visit_runs_in(sheet, col, rect.r0, rect.r1, &mut |h| {
16004                    let run = ids.run(h);
16005                    let r0 = run.row_start.max(rect.r0);
16006                    let r1 = (run.row_start + run.len - 1).min(rect.r1);
16007                    for row in r0..=r1 {
16008                        if !visited.contains(&(sheet, row, col)) {
16009                            stack.push(((sheet, row, col), run.first_id + (row - run.row_start)));
16010                        }
16011                    }
16012                });
16013            }
16014        };
16015        for &v in targets {
16016            if let Some(table) = self.graph.table_by_vertex(v) {
16017                // A table's demand is its range's, as legacy's table vertex
16018                // leads to the cells it covers (its symbol row has no
16019                // precedents).
16020                let (s, e) = (table.range.start, table.range.end);
16021                push_formulas(
16022                    &mut stack,
16023                    &visited,
16024                    s.sheet_id,
16025                    Rect::new(s.coord.row(), s.coord.col(), e.coord.row(), e.coord.col()),
16026                );
16027            } else if let Some(cell) = self.graph.authority_cell_of_vertex(v) {
16028                stack.push((cell, crate::engine::authority::identity::NO_VID));
16029            }
16030        }
16031        let mut hits = Vec::new();
16032        #[cfg(any(test, feature = "benchmark_internal"))]
16033        let (mut probe_vertices, mut probe_clean_formulas, mut probe_edges, mut probe_dynamic) =
16034            (0, 0, 0, 0);
16035        while let Some((cell, id)) = stack.pop() {
16036            if !visited.insert(cell) {
16037                continue;
16038            }
16039            let Some(v) = self.graph.authority_vertex_of_formula(id, cell) else {
16040                continue;
16041            };
16042            if !self.graph.vertex_exists(v) {
16043                continue;
16044            }
16045            #[cfg(any(test, feature = "benchmark_internal"))]
16046            {
16047                probe_vertices += 1;
16048            }
16049            match self.graph.get_vertex_kind(v) {
16050                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
16051                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
16052                        to_evaluate.insert(v);
16053                    } else {
16054                        #[cfg(any(test, feature = "benchmark_internal"))]
16055                        {
16056                            probe_clean_formulas += 1;
16057                        }
16058                    }
16059                }
16060                VertexKind::NamedScalar | VertexKind::NamedArray => {
16061                    to_evaluate.insert(v);
16062                }
16063                _ => {}
16064            }
16065            hits.clear();
16066            store.direct_precedents(cell, TagFilter::All, &mut hits);
16067            #[cfg(any(test, feature = "benchmark_internal"))]
16068            {
16069                probe_edges += hits.len();
16070            }
16071            for &(_, sheet, rect) in &hits {
16072                // DirtyExtents: a dirty spill anchor whose extent meets this
16073                // image is a demand precedent, ordered before `v` (§8.2).
16074                if sheet != SYMBOL_SHEET {
16075                    for anchor in self
16076                        .graph
16077                        .spill_anchors_in_region(sheet, rect.r0, rect.c0, rect.r1, rect.c1)
16078                    {
16079                        if anchor != v && self.graph.is_dirty(anchor) {
16080                            vdeps.entry(v).or_default().push(anchor);
16081                            if let Some(c) = self.graph.authority_cell_of_vertex(anchor) {
16082                                stack.push((c, crate::engine::authority::identity::NO_VID));
16083                            }
16084                        }
16085                    }
16086                }
16087                if sheet == SYMBOL_SHEET {
16088                    stack.extend((rect.r0..=rect.r1).map(|slot| {
16089                        (
16090                            (SYMBOL_SHEET, slot, 0),
16091                            crate::engine::authority::identity::NO_VID,
16092                        )
16093                    }));
16094                    continue;
16095                }
16096                push_formulas(&mut stack, &visited, sheet, rect);
16097            }
16098            if self.graph.is_dynamic(v) {
16099                #[cfg(any(test, feature = "benchmark_internal"))]
16100                {
16101                    probe_dynamic += 1;
16102                }
16103                // rdi_dyn: the observed reads are demand precedents.
16104                if let Some(reads) = self.graph.authority_host().observed(v) {
16105                    for &(sheet, r0, c0, r1, c1) in reads {
16106                        push_formulas(&mut stack, &visited, sheet, Rect::new(r0, c0, r1, c1));
16107                    }
16108                }
16109                let (vdeps_map, _) = VirtualDepBuilder::new(self).build(&[v]);
16110                // Pre-probe targets plus reads this request found dirty
16111                // (design §8.3: demand walks rdi ∪ hints).
16112                let hinted = self.freshness_hints(v).unwrap_or(&[]);
16113                if let Some(deps) = vdeps_map
16114                    .get(&v)
16115                    .map(|deps| deps.iter().chain(hinted))
16116                    .or(Some([].iter().chain(hinted)))
16117                {
16118                    for &u in deps {
16119                        vdeps.entry(v).or_default().push(u);
16120                        if let Some(c) = self.graph.authority_cell_of_vertex(u) {
16121                            stack.push((c, crate::engine::authority::identity::NO_VID));
16122                        }
16123                    }
16124                }
16125            }
16126        }
16127        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
16128        result.sort_unstable();
16129        for deps in vdeps.values_mut() {
16130            deps.sort_unstable();
16131            deps.dedup();
16132        }
16133        #[cfg(any(test, feature = "benchmark_internal"))]
16134        {
16135            let mut probe = self.recalc_reuse_probe.lock().unwrap();
16136            probe.demand_builds += 1;
16137            probe.demand_vertices += probe_vertices;
16138            probe.demand_clean_formulas += probe_clean_formulas;
16139            probe.demand_explicit_edges += probe_edges;
16140            probe.demand_virtual_builder_calls += probe_dynamic;
16141        }
16142        Ok((result, vdeps))
16143    }
16144
16145    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
16146    fn col_to_letters(col: u32) -> String {
16147        col_letters_from_1based(col).expect("column index must be >= 1")
16148    }
16149
16150    /// Evaluate all dirty/volatile vertices with cancellation support
16151    pub fn evaluate_all_cancellable(
16152        &mut self,
16153        cancel: crate::engine::CancelToken,
16154    ) -> Result<EvalResult, ExcelError> {
16155        self.observe_evaluation_resource_request(EvaluationRequestKind::FullCancellable, |engine| {
16156            engine.observe_function_semantic_epoch()?;
16157            engine.active_cancel_flag = Some(cancel.clone());
16158            let res = engine.evaluate_all_cancellable_impl(cancel.as_flag());
16159            engine.active_cancel_flag = None;
16160            res
16161        })
16162    }
16163
16164    fn evaluate_all_cancellable_impl(
16165        &mut self,
16166        cancel_flag: &AtomicBool,
16167    ) -> Result<EvalResult, ExcelError> {
16168        let _source_cache = self.source_cache_session();
16169        self.validate_deterministic_mode()?;
16170        if self.config.defer_graph_building {
16171            self.build_graph_all()?;
16172        }
16173        if cancel_flag.load(Ordering::Relaxed) {
16174            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16175                .with_message("Evaluation cancelled before scheduling".to_string()));
16176        }
16177        self.require_unified_authority()?;
16178        self.begin_evaluation_request();
16179        self.reset_virtual_dep_telemetry_if_disabled();
16180        let start = crate::instant::FzInstant::now();
16181        let mut computed_vertices = 0;
16182        let mut cycle_errors = 0;
16183
16184        let mut replan_iterations = 0;
16185        const MAX_REPLAN: usize = 5;
16186        let mut telemetry = self
16187            .config
16188            .enable_virtual_dep_telemetry
16189            .then(|| self.start_virtual_dep_telemetry());
16190
16191        loop {
16192            if cancel_flag.load(Ordering::Relaxed) {
16193                if let Some(mut t) = telemetry {
16194                    t.bailout_reason = Some("cancelled");
16195                    t.replan_iterations = replan_iterations;
16196                    self.last_virtual_dep_telemetry = t;
16197                }
16198                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16199                    .with_message("Evaluation cancelled before scheduling".to_string()));
16200            }
16201
16202            let to_evaluate = self.graph.get_evaluation_vertices();
16203            if to_evaluate.is_empty() {
16204                if let Some(t) = telemetry.as_mut()
16205                    && t.bailout_reason.is_none()
16206                {
16207                    t.bailout_reason = Some("no_work");
16208                }
16209                break;
16210            }
16211
16212            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16213            if let Some(t) = telemetry.as_mut() {
16214                Self::accumulate_schedule_meta(t, &meta);
16215            }
16216
16217            // Walk units in condensation order, checking cancellation between
16218            // units (formerly between cycles and between layers).
16219            self.begin_pass(&schedule);
16220            for (unit_index, &unit) in schedule.units.iter().enumerate() {
16221                match unit {
16222                    ScheduleUnit::Cycle(i) => {
16223                        // Check cancellation between cycles
16224                        if cancel_flag.load(Ordering::Relaxed) {
16225                            if let Some(mut t) = telemetry {
16226                                t.bailout_reason = Some("cancelled");
16227                                t.replan_iterations = replan_iterations;
16228                                self.last_virtual_dep_telemetry = t;
16229                            }
16230                            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
16231                                "Evaluation cancelled during cycle handling".to_string(),
16232                            ));
16233                        }
16234
16235                        if self.handle_cycle_unit(
16236                            schedule.unit_cycle(i),
16237                            None,
16238                            None,
16239                            Some(cancel_flag),
16240                        )? > 0
16241                        {
16242                            cycle_errors += 1;
16243                        }
16244                    }
16245                    ScheduleUnit::Layer(i) => {
16246                        let layer = schedule.unit_layer(i);
16247                        // Check cancellation between layers
16248                        if cancel_flag.load(Ordering::Relaxed) {
16249                            if let Some(mut t) = telemetry {
16250                                t.bailout_reason = Some("cancelled");
16251                                t.replan_iterations = replan_iterations;
16252                                self.last_virtual_dep_telemetry = t;
16253                            }
16254                            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16255                                .with_message("Evaluation cancelled between layers".to_string()));
16256                        }
16257
16258                        // Evaluate vertices in this layer (parallel or sequential)
16259                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16260                            computed_vertices +=
16261                                self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
16262                        } else {
16263                            computed_vertices +=
16264                                self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
16265                        }
16266                    }
16267                }
16268                if self.stop_after_unit(&schedule, unit_index) {
16269                    break;
16270                }
16271            }
16272
16273            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16274            if let Some(t) = telemetry.as_mut() {
16275                t.changed_vdeps_total += changed_vertices.len();
16276            }
16277            self.resource_checkpoint(0)?;
16278            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16279                if let Some(t) = telemetry.as_mut() {
16280                    t.bailout_reason = Some("converged");
16281                }
16282                break;
16283            }
16284            if replan_iterations >= MAX_REPLAN {
16285                if let Some(mut t) = telemetry.take() {
16286                    t.bailout_reason = Some("max_replan");
16287                    t.replan_iterations = replan_iterations;
16288                    self.last_virtual_dep_telemetry = t;
16289                }
16290                return Err(
16291                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16292                );
16293            }
16294            replan_iterations += 1;
16295        }
16296
16297        if let Some(mut t) = telemetry {
16298            t.replan_iterations = replan_iterations;
16299            self.last_virtual_dep_telemetry = t;
16300        }
16301
16302        // Re-dirty volatile vertices for the next evaluation cycle
16303        self.redirty_for_next_recalc();
16304        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16305
16306        Ok(EvalResult {
16307            computed_vertices,
16308            cycle_errors,
16309            elapsed: start.elapsed(),
16310        })
16311    }
16312
16313    /// Evaluate only the necessary precedents for specific target cells with cancellation support
16314    pub fn evaluate_until_cancellable(
16315        &mut self,
16316        targets: &[&str],
16317        cancel: crate::engine::CancelToken,
16318    ) -> Result<EvalResult, ExcelError> {
16319        self.observe_evaluation_resource_request(
16320            EvaluationRequestKind::TargetedCancellable,
16321            |engine| {
16322                engine.observe_function_semantic_epoch()?;
16323                engine.active_cancel_flag = Some(cancel.clone());
16324                let res = engine.evaluate_until_cancellable_impl(targets, cancel.as_flag());
16325                engine.active_cancel_flag = None;
16326                res
16327            },
16328        )
16329    }
16330
16331    fn evaluate_until_cancellable_impl(
16332        &mut self,
16333        targets: &[&str],
16334        cancel_flag: &AtomicBool,
16335    ) -> Result<EvalResult, ExcelError> {
16336        if cancel_flag.load(Ordering::Relaxed) {
16337            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16338                .with_message("Evaluation cancelled before target preparation"));
16339        }
16340        let mut typed_targets = Vec::with_capacity(targets.len());
16341        for target in targets {
16342            let (sheet, row, col) = self.parse_a1_notation(target)?;
16343            self.graph.sheet_id_mut(&sheet);
16344            typed_targets.push(crate::engine::EvaluationTarget::Cell { sheet, row, col });
16345        }
16346        self.evaluate_mixed_targets(&typed_targets, None)
16347    }
16348
16349    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
16350        let mut quoted = false;
16351        let mut separator = None;
16352        let bytes = address.as_bytes();
16353        let mut index = 0usize;
16354        while index < bytes.len() {
16355            match bytes[index] {
16356                b'\'' => {
16357                    if quoted && bytes.get(index + 1) == Some(&b'\'') {
16358                        index = index.saturating_add(1);
16359                    } else {
16360                        quoted = !quoted;
16361                    }
16362                }
16363                b'!' if !quoted => separator = Some(index),
16364                _ => {}
16365            }
16366            index = index.saturating_add(1);
16367        }
16368        if quoted {
16369            return Err(ExcelError::new(ExcelErrorKind::Ref)
16370                .with_message(format!("Invalid quoted sheet reference `{address}`")));
16371        }
16372        let (sheet, cell_part) = match separator {
16373            Some(separator) => {
16374                let raw_sheet = &address[..separator];
16375                let sheet = if raw_sheet.starts_with('\'') && raw_sheet.ends_with('\'') {
16376                    raw_sheet[1..raw_sheet.len().saturating_sub(1)].replace("''", "'")
16377                } else {
16378                    raw_sheet.to_string()
16379                };
16380                (sheet, &address[separator + 1..])
16381            }
16382            None => (self.default_sheet_name().to_string(), address),
16383        };
16384
16385        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
16386            ExcelError::new(ExcelErrorKind::Ref)
16387                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
16388        })?;
16389
16390        Ok((sheet, row, col))
16391    }
16392
16393    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
16394    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
16395        use formualizer_parse::parser::ASTNodeType;
16396        match &ast.node_type {
16397            ASTNodeType::Function { name, args, .. } => {
16398                if let Some(func) = self
16399                    .get_function("", name)
16400                    .or_else(|| crate::function_registry::get("", name))
16401                    && func.caps().contains(crate::function::FnCaps::VOLATILE)
16402                {
16403                    return true;
16404                }
16405                args.iter()
16406                    .any(|arg| self.is_ast_volatile_with_provider(arg))
16407            }
16408            ASTNodeType::BinaryOp { left, right, .. } => {
16409                self.is_ast_volatile_with_provider(left)
16410                    || self.is_ast_volatile_with_provider(right)
16411            }
16412            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
16413            ASTNodeType::Array(rows) => rows.iter().any(|row| {
16414                row.iter()
16415                    .any(|cell| self.is_ast_volatile_with_provider(cell))
16416            }),
16417            _ => false,
16418        }
16419    }
16420
16421    /// Evaluate a layer sequentially
16422    fn evaluate_layer_sequential(
16423        &mut self,
16424        layer: &super::scheduler::Layer,
16425    ) -> Result<usize, ExcelError> {
16426        self.resource_checkpoint(layer.vertices.len() as u64)?;
16427        self.evaluate_layer_sequential_effects(layer)
16428    }
16429
16430    fn update_vertex_value_with_delta(
16431        &mut self,
16432        vertex_id: VertexId,
16433        new_value: LiteralValue,
16434        delta: &mut DeltaCollector,
16435    ) {
16436        if delta.mode != DeltaMode::Off
16437            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
16438        {
16439            let sheet_name = self.graph.sheet_name(cell.sheet_id);
16440            let old = self
16441                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
16442                .unwrap_or(LiteralValue::Empty);
16443            if old != new_value {
16444                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
16445            }
16446        }
16447        self.graph.update_vertex_value_ref(vertex_id, &new_value);
16448        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
16449    }
16450
16451    fn evaluate_layer_sequential_with_delta(
16452        &mut self,
16453        layer: &super::scheduler::Layer,
16454        delta: &mut DeltaCollector,
16455    ) -> Result<usize, ExcelError> {
16456        self.resource_checkpoint(layer.vertices.len() as u64)?;
16457        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
16458    }
16459
16460    /// Evaluate a layer sequentially with cancellation support
16461    fn evaluate_layer_sequential_cancellable(
16462        &mut self,
16463        layer: &super::scheduler::Layer,
16464        cancel_flag: &AtomicBool,
16465    ) -> Result<usize, ExcelError> {
16466        self.resource_checkpoint(layer.vertices.len() as u64)?;
16467        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
16468    }
16469
16470    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
16471    fn evaluate_layer_sequential_cancellable_demand_driven(
16472        &mut self,
16473        layer: &super::scheduler::Layer,
16474        cancel_flag: &AtomicBool,
16475    ) -> Result<usize, ExcelError> {
16476        self.resource_checkpoint(layer.vertices.len() as u64)?;
16477        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
16478    }
16479
16480    /// Evaluate a layer in parallel using the thread pool.
16481    ///
16482    /// Cost-adaptive: the layer starts sequentially in slices of doubling
16483    /// size (1, 2, 4, ... vertices, capped so a slice does not overshoot the
16484    /// probe) and hands the rest to the pool once the rest looks worth it
16485    /// (`PARALLEL_LAYER_WORTH` at the rate so far) or the probe
16486    /// (`PARALLEL_LAYER_PROBE`) is spent. A cheap layer never pays the pool's
16487    /// wake-up and join (most Enron layers are tens of µs of work); an
16488    /// expensive one goes parallel after a few vertices. Splitting a layer
16489    /// into consecutive sub-layers is a valid order: its vertices are
16490    /// independent.
16491    fn evaluate_layer_parallel(
16492        &mut self,
16493        layer: &super::scheduler::Layer,
16494    ) -> Result<usize, ExcelError> {
16495        if layer.sequential {
16496            return self.evaluate_layer_sequential(layer);
16497        }
16498        self.resource_checkpoint(layer.vertices.len() as u64)?;
16499        let len = layer.vertices.len();
16500        let buffered = buffer_layer_writes(layer);
16501        let (probe, worth) = (PARALLEL_LAYER_PROBE, PARALLEL_LAYER_WORTH);
16502        let start = crate::instant::FzInstant::now();
16503        let mut pos = 0usize;
16504        let mut step = 1usize;
16505        while pos < len {
16506            if pos > 0 {
16507                let elapsed = start.elapsed();
16508                // Rate so far (ns per vertex) and the rest at that rate.
16509                let per_vertex = elapsed.as_nanos() / pos as u128 + 1;
16510                let rest_estimate = per_vertex * (len - pos) as u128;
16511                if len - pos >= 2 && (elapsed >= probe || rest_estimate >= worth.as_nanos()) {
16512                    let rest = layer.sub_layer(pos, len);
16513                    // Expensive members (a SUMIF over a table) parallelize
16514                    // one per task; cheap ones keep runs of 8 together.
16515                    let min_chunk = if per_vertex >= EXPENSIVE_VERTEX_NS {
16516                        1
16517                    } else {
16518                        8
16519                    };
16520                    return Ok(pos + self.evaluate_layer_parallel_effects(&rest, min_chunk)?);
16521                }
16522                // Next slice: double, but no more than the rest of the probe
16523                // at the rate so far (a slice must not overshoot it).
16524                let fit = (probe.saturating_sub(elapsed).as_nanos() / per_vertex) as usize + 1;
16525                step = step.saturating_mul(2).min(fit);
16526            }
16527            let end = (pos + step).min(len);
16528            let slice = layer.sub_layer(pos, end);
16529            // A slice stops at the probe's end even if its members turn out
16530            // far more expensive than the rate so far predicted.
16531            pos += self.evaluate_layer_units_until(
16532                &slice,
16533                None,
16534                None,
16535                None,
16536                buffered,
16537                Some(start + probe),
16538            )?;
16539        }
16540        Ok(len)
16541    }
16542
16543    fn evaluate_layer_parallel_with_delta(
16544        &mut self,
16545        layer: &super::scheduler::Layer,
16546        delta: &mut DeltaCollector,
16547    ) -> Result<usize, ExcelError> {
16548        if layer.sequential {
16549            return self.evaluate_layer_sequential_with_delta(layer, delta);
16550        }
16551        self.resource_checkpoint(layer.vertices.len() as u64)?;
16552        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
16553    }
16554
16555    /// Evaluate a layer in parallel with cancellation support
16556    fn evaluate_layer_parallel_cancellable(
16557        &mut self,
16558        layer: &super::scheduler::Layer,
16559        cancel_flag: &AtomicBool,
16560    ) -> Result<usize, ExcelError> {
16561        if layer.sequential {
16562            return self.evaluate_layer_sequential_cancellable(layer, cancel_flag);
16563        }
16564        self.resource_checkpoint(layer.vertices.len() as u64)?;
16565        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
16566    }
16567
16568    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
16569    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
16570        // Check if vertex exists
16571        if !self.graph.vertex_exists(vertex_id) {
16572            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
16573                .with_message(format!("Vertex not found: {vertex_id:?}")));
16574        }
16575
16576        // Get vertex kind and check if it needs evaluation
16577        let kind = self.graph.get_vertex_kind(vertex_id);
16578        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
16579
16580        let view = match kind {
16581            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
16582                if let Some(view) = self.graph.formula_view(vertex_id) {
16583                    view
16584                } else {
16585                    return Ok(LiteralValue::Number(0.0));
16586                }
16587            }
16588            VertexKind::Empty | VertexKind::Cell => {
16589                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
16590                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16591                    let row = cell_ref.coord.row() + 1;
16592                    let col = cell_ref.coord.col() + 1;
16593                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
16594                        return Ok(v);
16595                    }
16596                }
16597                return Ok(LiteralValue::Number(0.0));
16598            }
16599            VertexKind::NamedScalar => {
16600                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
16601                    ExcelError::new(ExcelErrorKind::Name)
16602                        .with_message("Named range metadata missing".to_string())
16603                })?;
16604
16605                return match &named_range.definition {
16606                    NamedDefinition::Cell(cell_ref) => {
16607                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16608                        Ok(self
16609                            .get_cell_value(
16610                                sheet_name,
16611                                cell_ref.coord.row() + 1,
16612                                cell_ref.coord.col() + 1,
16613                            )
16614                            .unwrap_or(LiteralValue::Empty))
16615                    }
16616                    NamedDefinition::Literal(v) => Ok(v.clone()),
16617                    NamedDefinition::Formula { ast, .. } => {
16618                        let context_sheet = match named_range.scope {
16619                            NameScope::Sheet(id) => id,
16620                            NameScope::Workbook => sheet_id,
16621                        };
16622                        let sheet_name = self.graph.sheet_name(context_sheet);
16623                        let cell_ref = self
16624                            .graph
16625                            .get_cell_ref(vertex_id)
16626                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
16627                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16628                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
16629                    }
16630                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
16631                        .with_message("Range-valued name evaluated as scalar".to_string())),
16632                };
16633            }
16634            VertexKind::NamedArray => {
16635                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
16636                    ExcelError::new(ExcelErrorKind::Name)
16637                        .with_message("Named range metadata missing".to_string())
16638                })?;
16639
16640                return match &named_range.definition {
16641                    NamedDefinition::Range(range_ref) => {
16642                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
16643                            return Err(ExcelError::new(ExcelErrorKind::Ref)
16644                                .with_message("Named range cannot span sheets".to_string()));
16645                        }
16646                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
16647                        let sr0 = range_ref.start.coord.row();
16648                        let sc0 = range_ref.start.coord.col();
16649                        let er0 = range_ref.end.coord.row();
16650                        let ec0 = range_ref.end.coord.col();
16651                        if sr0 > er0 || sc0 > ec0 {
16652                            return Err(ExcelError::new(ExcelErrorKind::Ref)
16653                                .with_message("Invalid named range bounds".to_string()));
16654                        }
16655
16656                        let h = (er0 - sr0 + 1) as usize;
16657                        let w = (ec0 - sc0 + 1) as usize;
16658                        let cell_count = (h as u64).saturating_mul(w as u64);
16659                        if cell_count > self.config.spill.max_spill_cells as u64 {
16660                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
16661                                "Named range too large to materialize as an array".to_string(),
16662                            ));
16663                        }
16664
16665                        // `get_cell_value` per cell, with the sheet resolved
16666                        // once (`read_cell_formatted_in` is its body).
16667                        let sheet_id = range_ref.start.sheet_id;
16668                        let asheet = self.arrow_sheets.sheet(sheet_name);
16669                        let mut rows = Vec::with_capacity(h);
16670                        for r0 in sr0..=er0 {
16671                            let mut row = Vec::with_capacity(w);
16672                            for c0 in sc0..=ec0 {
16673                                row.push(
16674                                    self.read_cell_formatted_in(sheet_id, asheet, r0 + 1, c0 + 1)
16675                                        .0,
16676                                );
16677                            }
16678                            rows.push(row);
16679                        }
16680                        Ok(LiteralValue::Array(rows))
16681                    }
16682                    NamedDefinition::Cell(cell_ref) => {
16683                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16684                        let row = cell_ref.coord.row() + 1;
16685                        let col = cell_ref.coord.col() + 1;
16686                        let v = self
16687                            .get_cell_value(sheet_name, row, col)
16688                            .unwrap_or(LiteralValue::Empty);
16689                        Ok(LiteralValue::Array(vec![vec![v]]))
16690                    }
16691                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
16692                    NamedDefinition::Formula { ast, .. } => {
16693                        let context_sheet = match named_range.scope {
16694                            NameScope::Sheet(id) => id,
16695                            NameScope::Workbook => sheet_id,
16696                        };
16697                        let sheet_name = self.graph.sheet_name(context_sheet);
16698                        let cell_ref = self
16699                            .graph
16700                            .get_cell_ref(vertex_id)
16701                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
16702                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16703                        match interpreter.evaluate_ast(ast) {
16704                            Ok(cv) => {
16705                                let v = cv.into_literal();
16706                                match v {
16707                                    LiteralValue::Array(_) => Ok(v),
16708                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
16709                                }
16710                            }
16711                            Err(err) => Ok(LiteralValue::Error(err)),
16712                        }
16713                    }
16714                };
16715            }
16716            VertexKind::InfiniteRange
16717            | VertexKind::Range
16718            | VertexKind::External
16719            | VertexKind::Table => {
16720                // Not directly evaluatable here.
16721                return Ok(LiteralValue::Number(0.0));
16722            }
16723        };
16724
16725        // The interpreter uses a reference to the engine as the context
16726        let sheet_name = self.graph.sheet_name(sheet_id);
16727        let cell_ref = self
16728            .graph
16729            .get_cell_ref(vertex_id)
16730            .expect("cell ref for vertex");
16731        if let Some(result) =
16732            self.freshness_evaluate_recorded(vertex_id, sheet_name, cell_ref, view)
16733        {
16734            return result;
16735        }
16736        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16737
16738        interpreter
16739            .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
16740            .map(|cv| {
16741                let format = cv.format_id();
16742                self.record_derived_format(vertex_id, format);
16743                crate::engine::result_finalization::finalize_formula_result(cv.into_literal())
16744            })
16745    }
16746
16747    /// Get access to the shared thread pool for parallel evaluation
16748    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
16749        self.thread_pool.as_ref()
16750    }
16751}
16752
16753#[derive(Default)]
16754struct RowBoundsCache {
16755    snapshot: u64,
16756    // key: (sheet_id, col_idx)
16757    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
16758}
16759
16760impl RowBoundsCache {
16761    fn new(snapshot: u64) -> Self {
16762        Self {
16763            snapshot,
16764            map: Default::default(),
16765        }
16766    }
16767    fn get_row_bounds(
16768        &self,
16769        sheet_id: SheetId,
16770        col_idx: usize,
16771        snapshot: u64,
16772    ) -> Option<(Option<u32>, Option<u32>)> {
16773        if self.snapshot != snapshot {
16774            return None;
16775        }
16776        self.map.get(&(sheet_id as u32, col_idx)).copied()
16777    }
16778    fn put_row_bounds(
16779        &mut self,
16780        sheet_id: SheetId,
16781        col_idx: usize,
16782        snapshot: u64,
16783        bounds: (Option<u32>, Option<u32>),
16784    ) {
16785        if self.snapshot != snapshot {
16786            self.snapshot = snapshot;
16787            self.map.clear();
16788        }
16789        self.map.insert((sheet_id as u32, col_idx), bounds);
16790    }
16791}
16792
16793struct UsedAxisBoundsCache {
16794    snapshot: u64,
16795    row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
16796    col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
16797    #[cfg(test)]
16798    row_hits: std::sync::atomic::AtomicUsize,
16799    #[cfg(test)]
16800    row_misses: std::sync::atomic::AtomicUsize,
16801    #[cfg(test)]
16802    col_hits: std::sync::atomic::AtomicUsize,
16803    #[cfg(test)]
16804    col_misses: std::sync::atomic::AtomicUsize,
16805}
16806
16807impl UsedAxisBoundsCache {
16808    fn new(snapshot: u64) -> Self {
16809        Self {
16810            snapshot,
16811            row_bounds_by_col_span: Default::default(),
16812            col_bounds_by_row_span: Default::default(),
16813            #[cfg(test)]
16814            row_hits: std::sync::atomic::AtomicUsize::new(0),
16815            #[cfg(test)]
16816            row_misses: std::sync::atomic::AtomicUsize::new(0),
16817            #[cfg(test)]
16818            col_hits: std::sync::atomic::AtomicUsize::new(0),
16819            #[cfg(test)]
16820            col_misses: std::sync::atomic::AtomicUsize::new(0),
16821        }
16822    }
16823
16824    fn reset_for_snapshot(&mut self, snapshot: u64) {
16825        if self.snapshot != snapshot {
16826            self.snapshot = snapshot;
16827            self.row_bounds_by_col_span.clear();
16828            self.col_bounds_by_row_span.clear();
16829        }
16830    }
16831
16832    fn get_row_bounds(
16833        &self,
16834        sheet_id: SheetId,
16835        start_col: u32,
16836        end_col: u32,
16837        snapshot: u64,
16838    ) -> Option<Option<(u32, u32)>> {
16839        if self.snapshot != snapshot {
16840            return None;
16841        }
16842        let cached = self
16843            .row_bounds_by_col_span
16844            .get(&(sheet_id, start_col, end_col))
16845            .copied();
16846        #[cfg(test)]
16847        if cached.is_some() {
16848            self.row_hits.fetch_add(1, Ordering::Relaxed);
16849        }
16850        cached
16851    }
16852
16853    fn put_row_bounds(
16854        &mut self,
16855        sheet_id: SheetId,
16856        start_col: u32,
16857        end_col: u32,
16858        snapshot: u64,
16859        bounds: Option<(u32, u32)>,
16860    ) {
16861        self.reset_for_snapshot(snapshot);
16862        self.row_bounds_by_col_span
16863            .insert((sheet_id, start_col, end_col), bounds);
16864        #[cfg(test)]
16865        self.row_misses.fetch_add(1, Ordering::Relaxed);
16866    }
16867
16868    fn get_col_bounds(
16869        &self,
16870        sheet_id: SheetId,
16871        start_row: u32,
16872        end_row: u32,
16873        snapshot: u64,
16874    ) -> Option<Option<(u32, u32)>> {
16875        if self.snapshot != snapshot {
16876            return None;
16877        }
16878        let cached = self
16879            .col_bounds_by_row_span
16880            .get(&(sheet_id, start_row, end_row))
16881            .copied();
16882        #[cfg(test)]
16883        if cached.is_some() {
16884            self.col_hits.fetch_add(1, Ordering::Relaxed);
16885        }
16886        cached
16887    }
16888
16889    fn put_col_bounds(
16890        &mut self,
16891        sheet_id: SheetId,
16892        start_row: u32,
16893        end_row: u32,
16894        snapshot: u64,
16895        bounds: Option<(u32, u32)>,
16896    ) {
16897        self.reset_for_snapshot(snapshot);
16898        self.col_bounds_by_row_span
16899            .insert((sheet_id, start_row, end_row), bounds);
16900        #[cfg(test)]
16901        self.col_misses.fetch_add(1, Ordering::Relaxed);
16902    }
16903}
16904
16905// Phase 2 shim: in-process spill manager delegating to current graph methods.
16906#[derive(Default)]
16907pub struct ShimSpillManager {
16908    region_locks: RegionLockManager,
16909    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
16910}
16911
16912impl ShimSpillManager {
16913    pub(crate) fn reserve(
16914        &mut self,
16915        owner: VertexId,
16916        anchor_cell: CellRef,
16917        shape: SpillShape,
16918        _meta: SpillMeta,
16919    ) -> Result<(), ExcelError> {
16920        // Derive region from anchor + shape; enforce in-flight exclusivity only.
16921        let region = crate::engine::spill::Region {
16922            sheet_id: anchor_cell.sheet_id as u32,
16923            row_start: anchor_cell.coord.row(),
16924            row_end: anchor_cell
16925                .coord
16926                .row()
16927                .saturating_add(shape.rows)
16928                .saturating_sub(1),
16929            col_start: anchor_cell.coord.col(),
16930            col_end: anchor_cell
16931                .coord
16932                .col()
16933                .saturating_add(shape.cols)
16934                .saturating_sub(1),
16935        };
16936        match self.region_locks.reserve(region, owner) {
16937            Ok(id) => {
16938                if id != 0 {
16939                    self.active_locks.insert(owner, id);
16940                }
16941                Ok(())
16942            }
16943            Err(e) => Err(e),
16944        }
16945    }
16946
16947    /// Release any in-flight region reservation still held for `owner`.
16948    ///
16949    /// Reservations are normally released on commit/rollback, but if an anchor is
16950    /// abandoned without committing (e.g. cycle detection stamps it with #CIRC), a
16951    /// stale reservation could remain. This is a no-op when nothing is held.
16952    pub(crate) fn release_owner(&mut self, owner: VertexId) {
16953        if let Some(id) = self.active_locks.remove(&owner) {
16954            self.region_locks.release(id);
16955        }
16956    }
16957
16958    pub(crate) fn commit_array_with_value_probe<F>(
16959        &mut self,
16960        graph: &mut DependencyGraph,
16961        anchor_vertex: VertexId,
16962        targets: &[CellRef],
16963        rows: Vec<Vec<LiteralValue>>,
16964        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
16965        mut value_probe: F,
16966    ) -> Result<(), ExcelError>
16967    where
16968        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
16969    {
16970        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
16971
16972        // Re-run plan on concrete targets before committing to respect blockers.
16973        // This plan checks formula/spill ownership in the graph, but when the graph value cache
16974        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
16975        let plan_res = graph.plan_spill_region_allowing_formula_overwrite(
16976            anchor_vertex,
16977            targets,
16978            overwritable_formulas,
16979        );
16980        if let Err(e) = plan_res {
16981            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
16982                self.region_locks.release(id);
16983            }
16984            return Err(e);
16985        }
16986
16987        if !graph.value_cache_enabled() {
16988            // Compute expected spill shape from the target rectangle for diagnostics.
16989            let (expected_rows, expected_cols) = if targets.is_empty() {
16990                (0u32, 0u32)
16991            } else {
16992                let mut min_r = u32::MAX;
16993                let mut max_r = 0u32;
16994                let mut min_c = u32::MAX;
16995                let mut max_c = 0u32;
16996                for cell in targets {
16997                    let r = cell.coord.row();
16998                    let c = cell.coord.col();
16999                    min_r = min_r.min(r);
17000                    max_r = max_r.max(r);
17001                    min_c = min_c.min(c);
17002                    max_c = max_c.max(c);
17003                }
17004                (
17005                    max_r.saturating_sub(min_r).saturating_add(1),
17006                    max_c.saturating_sub(min_c).saturating_add(1),
17007                )
17008            };
17009
17010            let anchor_cell = graph
17011                .get_cell_ref(anchor_vertex)
17012                .expect("anchor cell ref for spill commit");
17013
17014            for cell in targets {
17015                // Never treat the anchor as a blocker.
17016                if *cell == anchor_cell {
17017                    continue;
17018                }
17019                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
17020                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
17021                    continue;
17022                }
17023                // Skip formula vertices in the target region; plan() handled them (or allowed).
17024                if let Some(vid) = graph.get_vertex_id_for_address(cell)
17025                    && vid != anchor_vertex
17026                {
17027                    match graph.get_vertex_kind(vid) {
17028                        crate::engine::vertex::VertexKind::FormulaScalar
17029                        | crate::engine::vertex::VertexKind::FormulaArray => {
17030                            // plan() already approved allowed overwrites.
17031                            continue;
17032                        }
17033                        _ => {}
17034                    }
17035                }
17036
17037                if let Some(v) = value_probe(graph, cell)
17038                    && !matches!(v, LiteralValue::Empty)
17039                {
17040                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17041                        self.region_locks.release(id);
17042                    }
17043                    return Err(ExcelError::new(ExcelErrorKind::Spill)
17044                        .with_message("BlockedByValue")
17045                        .with_extra(ExcelErrorExtra::Spill {
17046                            expected_rows,
17047                            expected_cols,
17048                        }));
17049                }
17050            }
17051        }
17052
17053        let commit_res = graph.commit_spill_region_atomic_with_fault(
17054            anchor_vertex,
17055            targets.to_vec(),
17056            rows,
17057            None,
17058        );
17059        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17060            self.region_locks.release(id);
17061        }
17062        commit_res.map(|_| ())
17063    }
17064
17065    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
17066    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
17067        &mut self,
17068        engine: &mut Engine<R>,
17069        anchor_vertex: VertexId,
17070        targets: &[CellRef],
17071        rows: Vec<Vec<LiteralValue>>,
17072        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
17073    ) -> Result<(), ExcelError> {
17074        if let Err(error) = engine.guard_pending_spill_commit(anchor_vertex, targets) {
17075            self.release_owner(anchor_vertex);
17076            return Err(error);
17077        }
17078        // Re-run plan on concrete targets before committing to respect blockers.
17079        let plan_res = engine.graph.plan_spill_region_allowing_formula_overwrite(
17080            anchor_vertex,
17081            targets,
17082            overwritable_formulas,
17083        );
17084        if let Err(e) = plan_res {
17085            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17086                self.region_locks.release(id);
17087            }
17088            return Err(e);
17089        }
17090
17091        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
17092            anchor_vertex,
17093            targets.to_vec(),
17094            rows.clone(),
17095            None,
17096        );
17097        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17098            self.region_locks.release(id);
17099        }
17100        commit_res.map(|_| ())?;
17101        engine
17102            .blocked_pending_spills
17103            .retain(|entry| entry.0 != anchor_vertex);
17104
17105        // Mirror into Arrow overlay when enabled
17106        if engine.config.arrow_storage_enabled
17107            && engine.config.delta_overlay_enabled
17108            && engine.config.write_formula_overlay_enabled
17109        {
17110            // Expect targets to be a contiguous rectangle row-major starting at some anchor
17111            for (idx, cell) in targets.iter().enumerate() {
17112                let (r_off, c_off) = {
17113                    if rows.is_empty() || rows[0].is_empty() {
17114                        (0usize, 0usize)
17115                    } else {
17116                        let width = rows[0].len();
17117                        (idx / width, idx % width)
17118                    }
17119                };
17120                let v = rows
17121                    .get(r_off)
17122                    .and_then(|r| r.get(c_off))
17123                    .cloned()
17124                    .unwrap_or(LiteralValue::Empty);
17125                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
17126                engine.mirror_value_to_computed_overlay(
17127                    &sheet_name,
17128                    cell.coord.row() + 1,
17129                    cell.coord.col() + 1,
17130                    &v,
17131                );
17132            }
17133        }
17134        Ok(())
17135    }
17136}
17137
17138impl<R> Engine<R>
17139where
17140    R: EvaluationContext,
17141{
17142    fn resolve_shared_ref(
17143        &self,
17144        reference: &ReferenceType,
17145        current_sheet: &str,
17146    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
17147        use formualizer_common::{
17148            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
17149            SheetRef as SharedRef,
17150        };
17151
17152        // Preserve anchor flags from the parsed reference when possible.
17153        let sr = match reference {
17154            ReferenceType::Cell {
17155                sheet,
17156                row,
17157                col,
17158                row_abs,
17159                col_abs,
17160            } => {
17161                let row0 = row
17162                    .checked_sub(1)
17163                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17164                let col0 = col
17165                    .checked_sub(1)
17166                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17167                let sheet_loc = match sheet.as_deref() {
17168                    Some(name) => SheetLocator::from_name(name),
17169                    None => SheetLocator::Current,
17170                };
17171                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
17172                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
17173            }
17174            ReferenceType::Range {
17175                sheet,
17176                start_row,
17177                start_col,
17178                end_row,
17179                end_col,
17180                start_row_abs,
17181                start_col_abs,
17182                end_row_abs,
17183                end_col_abs,
17184            } => {
17185                let sheet_loc = match sheet.as_deref() {
17186                    Some(name) => SheetLocator::from_name(name),
17187                    None => SheetLocator::Current,
17188                };
17189                let sr = start_row
17190                    .map(|r| {
17191                        r.checked_sub(1)
17192                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17193                    })
17194                    .transpose()?;
17195                let sc = start_col
17196                    .map(|c| {
17197                        c.checked_sub(1)
17198                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17199                    })
17200                    .transpose()?;
17201                let er = end_row
17202                    .map(|r| {
17203                        r.checked_sub(1)
17204                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17205                    })
17206                    .transpose()?;
17207                let ec = end_col
17208                    .map(|c| {
17209                        c.checked_sub(1)
17210                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17211                    })
17212                    .transpose()?;
17213                let range = SharedRangeRef::from_parts(
17214                    sheet_loc,
17215                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
17216                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
17217                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
17218                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
17219                )
17220                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
17221                SharedRef::Range(range)
17222            }
17223            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
17224        };
17225
17226        let current_id = self
17227            .graph
17228            .sheet_id(current_sheet)
17229            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17230
17231        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
17232            match loc {
17233                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
17234                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
17235                SheetLocator::Name(name) => {
17236                    let n = name.as_ref();
17237                    self.graph
17238                        .sheet_id(n)
17239                        .map(SheetLocator::Id)
17240                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17241                }
17242            }
17243        };
17244
17245        match sr {
17246            SharedRef::Cell(cell) => {
17247                let owned = cell.into_owned();
17248                let sheet = resolve_loc(owned.sheet)?;
17249                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
17250            }
17251            SharedRef::Range(range) => {
17252                let owned = range.into_owned();
17253                let sheet = resolve_loc(owned.sheet)?;
17254                Ok(SharedRef::Range(SharedRangeRef {
17255                    sheet,
17256                    start_row: owned.start_row,
17257                    start_col: owned.start_col,
17258                    end_row: owned.end_row,
17259                    end_col: owned.end_col,
17260                }))
17261            }
17262        }
17263    }
17264}
17265
17266// Implement the resolver traits for the Engine.
17267// This allows the interpreter to resolve references by querying the engine's graph.
17268impl<R> crate::traits::ReferenceResolver for Engine<R>
17269where
17270    R: EvaluationContext,
17271{
17272    fn resolve_cell_reference(
17273        &self,
17274        sheet: Option<&str>,
17275        row: u32,
17276        col: u32,
17277    ) -> Result<LiteralValue, ExcelError> {
17278        // This context-free trait method has no knowledge of the formula's
17279        // current sheet, so an unqualified (`None`) reference cannot be resolved
17280        // here. Previously this fell back to `default_sheet_name()`, which leaked
17281        // the reference onto an unrelated sheet (issue #110). Interpreter paths
17282        // already qualify references with the current sheet before reaching this
17283        // method (see `Interpreter::implicit_intersection_from_reference`), and
17284        // the sheet-aware scalar path goes through `resolve_cell_reference_value`
17285        // with an explicit `current_sheet`. Returning #REF! for an unqualified
17286        // reference here surfaces the missing context instead of silently
17287        // returning data from the wrong sheet.
17288        let Some(sheet_name) = sheet else {
17289            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
17290                "Unqualified cell reference resolved without sheet context".to_string(),
17291            ));
17292        };
17293        // Prefer engine's unified accessor which consults Arrow store for base values
17294        // and falls back to graph for formulas and stored values.
17295        if let Some(v) = self.get_cell_value(sheet_name, row, col) {
17296            Ok(v)
17297        } else {
17298            // Excel semantics: empty cell coerces to 0 in numeric contexts
17299            Ok(LiteralValue::Number(0.0))
17300        }
17301    }
17302}
17303
17304impl<R> crate::traits::RangeResolver for Engine<R>
17305where
17306    R: EvaluationContext,
17307{
17308    fn resolve_range_reference(
17309        &self,
17310        sheet: Option<&str>,
17311        sr: Option<u32>,
17312        sc: Option<u32>,
17313        er: Option<u32>,
17314        ec: Option<u32>,
17315    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
17316        // For now, delegate range resolution to the external resolver.
17317        // A future optimization could be to handle this within the graph.
17318        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
17319    }
17320}
17321
17322impl<R> crate::traits::NamedRangeResolver for Engine<R>
17323where
17324    R: EvaluationContext,
17325{
17326    fn resolve_named_range_reference(
17327        &self,
17328        name: &str,
17329    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
17330        self.resolver.resolve_named_range_reference(name)
17331    }
17332}
17333
17334impl<R> crate::traits::TableResolver for Engine<R>
17335where
17336    R: EvaluationContext,
17337{
17338    fn resolve_table_reference(
17339        &self,
17340        tref: &formualizer_parse::parser::TableReference,
17341    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17342        self.resolver.resolve_table_reference(tref)
17343    }
17344}
17345
17346impl<R> crate::traits::SourceResolver for Engine<R>
17347where
17348    R: EvaluationContext,
17349{
17350    fn source_scalar_version(&self, name: &str) -> Option<u64> {
17351        self.resolver.source_scalar_version(name)
17352    }
17353
17354    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
17355        self.resolver.resolve_source_scalar(name)
17356    }
17357
17358    fn source_table_version(&self, name: &str) -> Option<u64> {
17359        self.resolver.source_table_version(name)
17360    }
17361
17362    fn resolve_source_table(
17363        &self,
17364        name: &str,
17365    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17366        self.resolver.resolve_source_table(name)
17367    }
17368}
17369
17370// The Engine is a Resolver because it implements the constituent traits.
17371impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
17372
17373// The Engine provides functions by delegating to its internal resolver.
17374impl<R> crate::traits::FunctionProvider for Engine<R>
17375where
17376    R: EvaluationContext,
17377{
17378    fn planning_semantic_revision(&self) -> Option<u64> {
17379        self.resolver.planning_semantic_revision()
17380    }
17381
17382    fn get_function(
17383        &self,
17384        prefix: &str,
17385        name: &str,
17386    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17387        self.resolver.get_function(prefix, name)
17388    }
17389
17390    fn get_function_for_planning(
17391        &self,
17392        prefix: &str,
17393        name: &str,
17394    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17395        self.resolver.get_function_for_planning(prefix, name)
17396    }
17397}
17398
17399impl<R> Engine<R>
17400where
17401    R: EvaluationContext,
17402{
17403    /// Semantic used coordinates exclude graph-only dependency placeholders.
17404    ///
17405    /// Non-empty base/overlay/computed cells come from Arrow storage, while
17406    /// scalar and array formulas come from graph formula kinds even before
17407    /// their results are materialized. The legacy graph fallback is omitted:
17408    /// `load_packed_to_vertex` entries are either represented by those sources
17409    /// or are `Empty` dependency placeholders, not a third value authority.
17410    pub(crate) fn semantic_used_rows_for_columns(
17411        &self,
17412        sheet: &str,
17413        start_col: u32,
17414        end_col: u32,
17415    ) -> Option<(u32, u32)> {
17416        let arrow_bounds = self
17417            .sheet_store()
17418            .sheet(sheet)
17419            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
17420        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
17421        Self::union_used_bounds(arrow_bounds, formula_bounds)
17422    }
17423
17424    pub(crate) fn semantic_used_cols_for_rows(
17425        &self,
17426        sheet: &str,
17427        start_row: u32,
17428        end_row: u32,
17429    ) -> Option<(u32, u32)> {
17430        let arrow_bounds = self
17431            .sheet_store()
17432            .sheet(sheet)
17433            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
17434        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
17435        Self::union_used_bounds(arrow_bounds, formula_bounds)
17436    }
17437}
17438
17439// Override EvaluationContext to provide thread pool access
17440impl<R> crate::traits::EvaluationContext for Engine<R>
17441where
17442    R: EvaluationContext,
17443{
17444    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
17445        &self.clock
17446    }
17447
17448    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
17449        self.thread_pool.as_ref()
17450    }
17451
17452    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
17453        self.active_cancel_flag.clone()
17454    }
17455
17456    fn chunk_hint(&self) -> Option<usize> {
17457        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
17458        let hint =
17459            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
17460        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
17461    }
17462
17463    fn volatile_level(&self) -> crate::traits::VolatileLevel {
17464        self.config.volatile_level
17465    }
17466
17467    fn workbook_seed(&self) -> u64 {
17468        self.config.workbook_seed
17469    }
17470
17471    fn recalc_epoch(&self) -> u64 {
17472        self.recalc_epoch
17473    }
17474
17475    fn workbook_sheet_count(&self) -> Option<usize> {
17476        Some(self.graph.sheet_reg().active_len())
17477    }
17478
17479    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
17480        self.graph.sheet_reg().active_position(sheet)
17481    }
17482
17483    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
17484        self.sheet_index_by_name(current_sheet)
17485    }
17486
17487    fn inspect_reference(
17488        &self,
17489        reference: &ReferenceType,
17490        current_sheet: &str,
17491    ) -> Result<Option<ReferenceInfo>, ExcelError> {
17492        let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
17493            let sheet_id = self
17494                .graph
17495                .sheet_id(sheet_name)
17496                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17497            let sheet_index = self
17498                .graph
17499                .sheet_reg()
17500                .active_position_by_id(sheet_id)
17501                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17502            Ok((sheet_id, sheet_index))
17503        };
17504
17505        let cell_info =
17506            |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
17507                let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
17508                let row0 = row
17509                    .checked_sub(1)
17510                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17511                let col0 = col
17512                    .checked_sub(1)
17513                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17514                Ok(ReferenceInfo {
17515                    first_sheet_index: Some(sheet_index),
17516                    sheet_count: Some(1),
17517                    first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
17518                })
17519            };
17520
17521        let range_info = |sheet_name: &str,
17522                          start_row: Option<u32>,
17523                          start_col: Option<u32>|
17524         -> Result<ReferenceInfo, ExcelError> {
17525            let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
17526            let row = start_row.unwrap_or(1);
17527            let col = start_col.unwrap_or(1);
17528            let row0 = row
17529                .checked_sub(1)
17530                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17531            let col0 = col
17532                .checked_sub(1)
17533                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17534            Ok(ReferenceInfo {
17535                first_sheet_index: Some(sheet_index),
17536                sheet_count: Some(1),
17537                first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
17538            })
17539        };
17540
17541        let info = match reference {
17542            ReferenceType::Cell {
17543                sheet, row, col, ..
17544            } => {
17545                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
17546                cell_info(sheet_name, *row, *col)?
17547            }
17548            ReferenceType::Range {
17549                sheet,
17550                start_row,
17551                start_col,
17552                ..
17553            } => {
17554                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
17555                range_info(sheet_name, *start_row, *start_col)?
17556            }
17557            ReferenceType::Cell3D {
17558                sheet_first,
17559                sheet_last,
17560                row,
17561                col,
17562                ..
17563            } => {
17564                let first = cell_info(sheet_first, *row, *col)?;
17565                ReferenceInfo {
17566                    first_sheet_index: first.first_sheet_index,
17567                    sheet_count: self
17568                        .graph
17569                        .sheet_reg()
17570                        .active_span_len(sheet_first, sheet_last),
17571                    first_cell: first.first_cell,
17572                }
17573            }
17574            ReferenceType::Range3D {
17575                sheet_first,
17576                sheet_last,
17577                start_row,
17578                start_col,
17579                ..
17580            } => {
17581                let first = range_info(sheet_first, *start_row, *start_col)?;
17582                ReferenceInfo {
17583                    first_sheet_index: first.first_sheet_index,
17584                    sheet_count: self
17585                        .graph
17586                        .sheet_reg()
17587                        .active_span_len(sheet_first, sheet_last),
17588                    first_cell: first.first_cell,
17589                }
17590            }
17591            ReferenceType::NamedRange(name) => {
17592                let current_id = self
17593                    .graph
17594                    .sheet_id(current_sheet)
17595                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17596                let named = self
17597                    .graph
17598                    .resolve_name_entry(name, current_id)
17599                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17600                match &named.definition {
17601                    NamedDefinition::Cell(cell) => ReferenceInfo {
17602                        first_sheet_index: self
17603                            .graph
17604                            .sheet_reg()
17605                            .active_position_by_id(cell.sheet_id),
17606                        sheet_count: Some(1),
17607                        first_cell: Some(*cell),
17608                    },
17609                    NamedDefinition::Range(range) => ReferenceInfo {
17610                        first_sheet_index: self
17611                            .graph
17612                            .sheet_reg()
17613                            .active_position_by_id(range.start.sheet_id),
17614                        sheet_count: Some(1),
17615                        first_cell: Some(range.start),
17616                    },
17617                    NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
17618                        ReferenceInfo {
17619                            first_sheet_index: None,
17620                            sheet_count: None,
17621                            first_cell: None,
17622                        }
17623                    }
17624                }
17625            }
17626            ReferenceType::Table(tref) => {
17627                let table = self
17628                    .graph
17629                    .resolve_table_entry(&tref.name)
17630                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17631                ReferenceInfo {
17632                    first_sheet_index: self
17633                        .graph
17634                        .sheet_reg()
17635                        .active_position_by_id(table.range.start.sheet_id),
17636                    sheet_count: Some(1),
17637                    first_cell: Some(table.range.start),
17638                }
17639            }
17640            ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
17641        };
17642
17643        Ok(Some(info))
17644    }
17645
17646    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
17647        let sheet_name = self.graph.sheet_name(cell.sheet_id);
17648        if sheet_name.is_empty() {
17649            return Err(ExcelError::new(ExcelErrorKind::Ref));
17650        }
17651        let row = cell.coord.row() + 1;
17652        let col = cell.coord.col() + 1;
17653
17654        if let Some(entries) = self.staged_formulas.get(sheet_name)
17655            && let Some(text) = entries.get(row, col)
17656        {
17657            return Ok(Some(if text.starts_with('=') {
17658                text.to_owned()
17659            } else {
17660                format!("={text}")
17661            }));
17662        }
17663
17664        let Some((Some(ast), _)) = self.get_cell(sheet_name, row, col) else {
17665            return Ok(None);
17666        };
17667        Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
17668    }
17669
17670    fn used_rows_for_columns(
17671        &self,
17672        sheet: &str,
17673        start_col: u32,
17674        end_col: u32,
17675    ) -> Option<(u32, u32)> {
17676        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
17677        let sheet_id = self.graph.sheet_id(sheet)?;
17678        let snap = self.data_snapshot_id();
17679        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
17680            guard
17681                .as_ref()
17682                .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
17683        }) {
17684            return cached;
17685        }
17686
17687        let arrow_bounds = self
17688            .sheet_store()
17689            .sheet(sheet)
17690            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
17691        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
17692        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
17693            Some(bounds)
17694        } else {
17695            let sc0 = start_col.saturating_sub(1);
17696            let ec0 = end_col.saturating_sub(1);
17697            self.graph
17698                .used_row_bounds_for_columns(sheet_id, sc0, ec0)
17699                .map(|(a0, b0)| (a0 + 1, b0 + 1))
17700        };
17701
17702        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
17703            guard
17704                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
17705                .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
17706        }
17707
17708        computed
17709    }
17710
17711    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
17712        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
17713        let sheet_id = self.graph.sheet_id(sheet)?;
17714        let snap = self.data_snapshot_id();
17715        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
17716            guard
17717                .as_ref()
17718                .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
17719        }) {
17720            return cached;
17721        }
17722
17723        let arrow_bounds = self
17724            .sheet_store()
17725            .sheet(sheet)
17726            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
17727        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
17728        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
17729            Some(bounds)
17730        } else {
17731            let sr0 = start_row.saturating_sub(1);
17732            let er0 = end_row.saturating_sub(1);
17733            self.graph
17734                .used_col_bounds_for_rows(sheet_id, sr0, er0)
17735                .map(|(a0, b0)| (a0 + 1, b0 + 1))
17736        };
17737
17738        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
17739            guard
17740                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
17741                .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
17742        }
17743
17744        computed
17745    }
17746
17747    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
17748        let _ = self.graph.sheet_id(sheet)?;
17749        // Excel-like upper bounds; we expose something finite but large.
17750        // Backends may override with real bounds.
17751        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
17752    }
17753
17754    fn data_snapshot_id(&self) -> u64 {
17755        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
17756    }
17757
17758    fn backend_caps(&self) -> crate::traits::BackendCaps {
17759        crate::traits::BackendCaps {
17760            streaming: true,
17761            used_region: true,
17762            write: false,
17763            tables: false,
17764            async_stream: false,
17765        }
17766    }
17767
17768    fn build_lookup_index(
17769        &self,
17770        view: &RangeView<'_>,
17771        axis: LookupAxis,
17772    ) -> Option<Arc<LookupIndex>> {
17773        self.build_lookup_index_impl(view, axis)
17774    }
17775
17776    // Flats removed
17777
17778    fn date_system(&self) -> crate::engine::DateSystem {
17779        self.config.date_system
17780    }
17781    /// New: resolve a reference into a RangeView (Phase 2 API)
17782    fn resolve_range_view<'c>(
17783        &'c self,
17784        reference: &ReferenceType,
17785        current_sheet: &str,
17786    ) -> Result<RangeView<'c>, ExcelError> {
17787        match reference {
17788            ReferenceType::External(ext) => {
17789                let name = ext.raw.as_str();
17790                match ext.kind {
17791                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
17792                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
17793                            return Err(ExcelError::new(ExcelErrorKind::Name)
17794                                .with_message(format!("Undefined name: {name}")));
17795                        };
17796                        let version = source
17797                            .version
17798                            .or_else(|| self.resolver.source_scalar_version(name));
17799                        let v = self.resolve_source_scalar_cached(name, version)?;
17800                        Ok(RangeView::from_owned_rows(
17801                            vec![vec![v]],
17802                            self.config.date_system,
17803                        ))
17804                    }
17805                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
17806                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
17807                            // A deferred (whole-row/column) external range
17808                            // evaluates to #REF!, as it did before the parser
17809                            // kept it whole (see `unbound_external_range_defers`).
17810                            let kind =
17811                                if crate::engine::refs::unbound_external_range_defers(&ext.kind) {
17812                                    ExcelErrorKind::Ref
17813                                } else {
17814                                    ExcelErrorKind::Name
17815                                };
17816                            return Err(ExcelError::new(kind)
17817                                .with_message(format!("Undefined table: {name}")));
17818                        };
17819                        let version = source
17820                            .version
17821                            .or_else(|| self.resolver.source_table_version(name));
17822                        let table = self.resolve_source_table_cached(name, version)?;
17823                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
17824                        self.source_table_to_range_view(table.as_ref(), &spec)
17825                    }
17826                }
17827            }
17828            ReferenceType::Range { .. } => {
17829                let shared = self.resolve_shared_ref(reference, current_sheet)?;
17830                let formualizer_common::SheetRef::Range(range) = shared else {
17831                    return Err(ExcelError::new(ExcelErrorKind::Ref));
17832                };
17833                // No context sheet is available here, so an unresolved locator
17834                // is #REF! rather than a guess (issue #110).
17835                let sheet_id = match range.sheet {
17836                    formualizer_common::SheetLocator::Id(id) => id,
17837                    formualizer_common::SheetLocator::Current
17838                    | formualizer_common::SheetLocator::Name(_) => {
17839                        return Err(ExcelError::new(ExcelErrorKind::Ref));
17840                    }
17841                };
17842                let sheet_name = self.graph.sheet_name(sheet_id);
17843
17844                let bounded_range = if range.start_row.is_some()
17845                    && range.start_col.is_some()
17846                    && range.end_row.is_some()
17847                    && range.end_col.is_some()
17848                {
17849                    Some(RangeRef::try_from_shared(range.as_ref())?)
17850                } else {
17851                    None
17852                };
17853
17854                let sr = bounded_range
17855                    .as_ref()
17856                    .map(|r| r.start.coord.row() + 1)
17857                    .or_else(|| range.start_row.map(|b| b.index + 1));
17858                let sc = bounded_range
17859                    .as_ref()
17860                    .map(|r| r.start.coord.col() + 1)
17861                    .or_else(|| range.start_col.map(|b| b.index + 1));
17862                let er = bounded_range
17863                    .as_ref()
17864                    .map(|r| r.end.coord.row() + 1)
17865                    .or_else(|| range.end_row.map(|b| b.index + 1));
17866                let ec = bounded_range
17867                    .as_ref()
17868                    .map(|r| r.end.coord.col() + 1)
17869                    .or_else(|| range.end_col.map(|b| b.index + 1));
17870
17871                let extent = resolve_used_extent_with_fallback(
17872                    OpenRangeBounds {
17873                        start_row: sr,
17874                        start_column: sc,
17875                        end_row: er,
17876                        end_column: ec,
17877                    },
17878                    ExtentPolicy::EvaluationCompat {
17879                        fallback_row: None,
17880                        fallback_column: None,
17881                    },
17882                    || {
17883                        self.sheet_bounds(sheet_name)
17884                            .map(|_| self.config.max_open_ended_rows)
17885                    },
17886                    || {
17887                        self.sheet_bounds(sheet_name)
17888                            .map(|_| self.config.max_open_ended_cols)
17889                    },
17890                    |first, last| self.used_rows_for_columns(sheet_name, first, last),
17891                    |first, last| self.used_cols_for_rows(sheet_name, first, last),
17892                );
17893                let (sr, sc, er, ec) = extent
17894                    .map(|extent| {
17895                        (
17896                            extent.start_row,
17897                            extent.start_column,
17898                            extent.end_row,
17899                            extent.end_column,
17900                        )
17901                    })
17902                    .unwrap_or((1, 1, 0, 0));
17903
17904                if self.force_materialize_range_views {
17905                    if er < sr || ec < sc {
17906                        return Ok(RangeView::from_owned_rows(
17907                            Vec::new(),
17908                            self.config.date_system,
17909                        ));
17910                    }
17911                    let h = (er - sr + 1) as u64;
17912                    let w = (ec - sc + 1) as u64;
17913                    let cell_count = h.saturating_mul(w);
17914                    if cell_count <= self.config.spill.max_spill_cells as u64 {
17915                        let mut rows: Vec<Vec<LiteralValue>> = Vec::with_capacity(h as usize);
17916                        for r in sr..=er {
17917                            let mut rowv: Vec<LiteralValue> = Vec::with_capacity(w as usize);
17918                            for c in sc..=ec {
17919                                rowv.push(
17920                                    self.get_cell_value(sheet_name, r, c)
17921                                        .unwrap_or(LiteralValue::Empty),
17922                                );
17923                            }
17924                            rows.push(rowv);
17925                        }
17926                        return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
17927                    }
17928                }
17929
17930                let Some(asheet) = self.sheet_store().sheet(sheet_name) else {
17931                    return Ok(RangeView::from_owned_rows(
17932                        Vec::new(),
17933                        self.config.date_system,
17934                    ));
17935                };
17936
17937                let rv = if er < sr || ec < sc {
17938                    asheet.range_view(1, 1, 0, 0)
17939                } else {
17940                    let sr0 = sr.saturating_sub(1) as usize;
17941                    let sc0 = sc.saturating_sub(1) as usize;
17942                    let er0 = er.saturating_sub(1) as usize;
17943                    let ec0 = ec.saturating_sub(1) as usize;
17944                    asheet.range_view(sr0, sc0, er0, ec0)
17945                };
17946
17947                Ok(rv)
17948            }
17949            ReferenceType::Cell { .. } => {
17950                let shared = self.resolve_shared_ref(reference, current_sheet)?;
17951                let formualizer_common::SheetRef::Cell(cell) = shared else {
17952                    return Err(ExcelError::new(ExcelErrorKind::Ref));
17953                };
17954                let addr = CellRef::try_from_shared(cell)?;
17955                let sheet_id = addr.sheet_id;
17956                let sheet_name = self.graph.sheet_name(sheet_id);
17957                let row = addr.coord.row() + 1;
17958                let col = addr.coord.col() + 1;
17959
17960                if self.force_materialize_range_views {
17961                    let v = self
17962                        .get_cell_value(sheet_name, row, col)
17963                        .unwrap_or(LiteralValue::Empty);
17964                    return Ok(RangeView::from_owned_rows(
17965                        vec![vec![v]],
17966                        self.config.date_system,
17967                    ));
17968                }
17969
17970                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
17971                    let r0 = row.saturating_sub(1) as usize;
17972                    let c0 = col.saturating_sub(1) as usize;
17973                    let rv = asheet.range_view(r0, c0, r0, c0);
17974                    Ok(rv)
17975                } else {
17976                    let v = self
17977                        .get_cell_value(sheet_name, row, col)
17978                        .unwrap_or(LiteralValue::Empty);
17979                    Ok(RangeView::from_owned_rows(
17980                        vec![vec![v]],
17981                        self.config.date_system,
17982                    ))
17983                }
17984            }
17985            ReferenceType::NamedRange(name) => {
17986                if let Some(current_id) = self.graph.sheet_id(current_sheet)
17987                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
17988                {
17989                    match &named.definition {
17990                        NamedDefinition::Cell(cell_ref) => {
17991                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17992                            if self.force_materialize_range_views {
17993                                let v = self
17994                                    .get_cell_value(
17995                                        sheet_name,
17996                                        cell_ref.coord.row() + 1,
17997                                        cell_ref.coord.col() + 1,
17998                                    )
17999                                    .unwrap_or(LiteralValue::Empty);
18000                                return Ok(RangeView::from_owned_rows(
18001                                    vec![vec![v]],
18002                                    self.config.date_system,
18003                                ));
18004                            } else {
18005                                let asheet = self
18006                                    .sheet_store()
18007                                    .sheet(sheet_name)
18008                                    .expect("Arrow sheet missing for named cell");
18009                                let r0 = cell_ref.coord.row() as usize;
18010                                let c0 = cell_ref.coord.col() as usize;
18011                                let rv = asheet.range_view(r0, c0, r0, c0);
18012                                return Ok(rv);
18013                            }
18014                        }
18015                        NamedDefinition::Range(range_ref) => {
18016                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
18017                            let sr = range_ref.start.coord.row() + 1;
18018                            let sc = range_ref.start.coord.col() + 1;
18019                            let er = range_ref.end.coord.row() + 1;
18020                            let ec = range_ref.end.coord.col() + 1;
18021                            if self.force_materialize_range_views {
18022                                let h = (er.saturating_sub(sr) + 1) as u64;
18023                                let w = (ec.saturating_sub(sc) + 1) as u64;
18024                                let cell_count = h.saturating_mul(w);
18025                                if cell_count <= self.config.spill.max_spill_cells as u64 {
18026                                    let mut rows: Vec<Vec<LiteralValue>> =
18027                                        Vec::with_capacity(h as usize);
18028                                    for r in sr..=er {
18029                                        let mut rowv: Vec<LiteralValue> =
18030                                            Vec::with_capacity(w as usize);
18031                                        for c in sc..=ec {
18032                                            rowv.push(
18033                                                self.get_cell_value(sheet_name, r, c)
18034                                                    .unwrap_or(LiteralValue::Empty),
18035                                            );
18036                                        }
18037                                        rows.push(rowv);
18038                                    }
18039                                    return Ok(RangeView::from_owned_rows(
18040                                        rows,
18041                                        self.config.date_system,
18042                                    ));
18043                                }
18044                            }
18045                            let asheet = self
18046                                .sheet_store()
18047                                .sheet(sheet_name)
18048                                .expect("Arrow sheet missing for named range");
18049                            let sr0 = range_ref.start.coord.row() as usize;
18050                            let sc0 = range_ref.start.coord.col() as usize;
18051                            let er0 = range_ref.end.coord.row() as usize;
18052                            let ec0 = range_ref.end.coord.col() as usize;
18053                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
18054                            return Ok(rv);
18055                        }
18056                        NamedDefinition::Literal(v) => {
18057                            return Ok(RangeView::from_owned_rows(
18058                                vec![vec![v.clone()]],
18059                                self.config.date_system,
18060                            ));
18061                        }
18062                        NamedDefinition::Formula { .. } => {
18063                            if let Some(value) = self.graph.get_value(named.vertex) {
18064                                return Ok(RangeView::from_owned_rows(
18065                                    vec![vec![value]],
18066                                    self.config.date_system,
18067                                ));
18068                            }
18069                        }
18070                    }
18071                }
18072
18073                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
18074                    let version = source
18075                        .version
18076                        .or_else(|| self.resolver.source_scalar_version(name));
18077                    let v = self.resolve_source_scalar_cached(name, version)?;
18078                    return Ok(RangeView::from_owned_rows(
18079                        vec![vec![v]],
18080                        self.config.date_system,
18081                    ));
18082                }
18083
18084                let data = self.resolver.resolve_named_range_reference(name)?;
18085                Ok(RangeView::from_owned_rows(data, self.config.date_system))
18086            }
18087            ReferenceType::Table(tref) => {
18088                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
18089                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
18090                    let asheet = self
18091                        .sheet_store()
18092                        .sheet(sheet_name)
18093                        .expect("Arrow sheet missing for table reference");
18094
18095                    let sr0 = table.range.start.coord.row() as usize;
18096                    let sc0 = table.range.start.coord.col() as usize;
18097                    let er0 = table.range.end.coord.row() as usize;
18098                    let ec0 = table.range.end.coord.col() as usize;
18099
18100                    let has_totals = table.totals_row;
18101                    let has_headers = table.header_row;
18102                    let data_sr = if has_headers {
18103                        sr0.saturating_add(1)
18104                    } else {
18105                        sr0
18106                    };
18107                    let data_er = if has_totals {
18108                        er0.saturating_sub(1)
18109                    } else {
18110                        er0
18111                    };
18112
18113                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
18114                        if sr > er || sc > ec {
18115                            asheet.range_view(1, 1, 0, 0)
18116                        } else {
18117                            asheet.range_view(sr, sc, er, ec)
18118                        }
18119                    };
18120
18121                    let av = match &tref.specifier {
18122                        None => {
18123                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18124                                "Table reference without specifier is unsupported".to_string(),
18125                            ));
18126                        }
18127                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
18128                            let Some(idx) = table.col_index(col) else {
18129                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18130                                    "Column refers to unknown table column".to_string(),
18131                                ));
18132                            };
18133                            let c0 = sc0 + idx;
18134                            select(data_sr, c0, data_er, c0)
18135                        }
18136                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
18137                            start,
18138                            end,
18139                        )) => {
18140                            let Some(si) = table.col_index(start) else {
18141                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18142                                    "Column range refers to unknown column(s)".to_string(),
18143                                ));
18144                            };
18145                            let Some(ei) = table.col_index(end) else {
18146                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18147                                    "Column range refers to unknown column(s)".to_string(),
18148                                ));
18149                            };
18150                            let (mut a, mut b) = (si, ei);
18151                            if a > b {
18152                                std::mem::swap(&mut a, &mut b);
18153                            }
18154                            let c_start = sc0 + a;
18155                            let c_end = sc0 + b;
18156                            select(data_sr, c_start, data_er, c_end)
18157                        }
18158                        Some(formualizer_parse::parser::TableSpecifier::All)
18159                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18160                            formualizer_parse::parser::SpecialItem::All,
18161                        )) => select(sr0, sc0, er0, ec0),
18162                        Some(formualizer_parse::parser::TableSpecifier::Data)
18163                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18164                            formualizer_parse::parser::SpecialItem::Data,
18165                        )) => select(data_sr, sc0, data_er, ec0),
18166                        Some(formualizer_parse::parser::TableSpecifier::Headers)
18167                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18168                            formualizer_parse::parser::SpecialItem::Headers,
18169                        )) => {
18170                            if !has_headers {
18171                                asheet.range_view(1, 1, 0, 0)
18172                            } else {
18173                                select(sr0, sc0, sr0, ec0)
18174                            }
18175                        }
18176                        Some(formualizer_parse::parser::TableSpecifier::Totals)
18177                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18178                            formualizer_parse::parser::SpecialItem::Totals,
18179                        )) => {
18180                            if !has_totals {
18181                                asheet.range_view(1, 1, 0, 0)
18182                            } else {
18183                                select(er0, sc0, er0, ec0)
18184                            }
18185                        }
18186                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18187                            formualizer_parse::parser::SpecialItem::ThisRow,
18188                        )) => {
18189                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18190                                "@ (This Row) requires table-aware context; not yet supported"
18191                                    .to_string(),
18192                            ));
18193                        }
18194                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
18195                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
18196                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18197                                "Complex structured references not yet supported".to_string(),
18198                            ));
18199                        }
18200                    };
18201
18202                    return Ok(av);
18203                }
18204
18205                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
18206                    let version = source
18207                        .version
18208                        .or_else(|| self.resolver.source_table_version(&tref.name));
18209                    let table = self.resolve_source_table_cached(&tref.name, version)?;
18210                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
18211                }
18212
18213                // Fallback: materialize via Resolver::resolve_range_like tranche 1.
18214                // A table nobody defines (an unbound reference kept by the
18215                // BestEffort preparation policy) is `#NAME?`, the kind Strict
18216                // reports at preparation, not the resolver's "not implemented".
18217                let boxed = self
18218                    .resolve_range_like(&ReferenceType::Table(tref.clone()))
18219                    .map_err(|e| {
18220                        if e.kind == ExcelErrorKind::NImpl {
18221                            ExcelError::new(ExcelErrorKind::Name)
18222                                .with_message(format!("Unknown table: {}", tref.name))
18223                        } else {
18224                            e
18225                        }
18226                    })?;
18227                let owned = boxed.materialise().into_owned();
18228                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
18229            }
18230            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
18231                Err(ExcelError::new(ExcelErrorKind::NImpl)
18232                    .with_message("3D references are not yet supported".to_string()))
18233            }
18234        }
18235    }
18236
18237    fn resolve_cell_format(
18238        &self,
18239        sheet: Option<&str>,
18240        row: u32,
18241        col: u32,
18242        current_sheet: &str,
18243    ) -> Option<crate::format::FormatId> {
18244        self.effective_format_id(sheet.unwrap_or(current_sheet), row, col)
18245    }
18246
18247    fn format_class(
18248        &self,
18249        format: crate::format::FormatId,
18250    ) -> Option<formualizer_common::numfmt::FormatClass> {
18251        self.format_registry.class(format).cloned()
18252    }
18253
18254    fn record_cell_derived_format(
18255        &self,
18256        sheet: &str,
18257        row: u32,
18258        col: u32,
18259        format: Option<crate::format::FormatId>,
18260    ) {
18261        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
18262            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
18263            self.record_derived_format_at(cell, format);
18264        }
18265    }
18266
18267    fn resolve_cell_reference_value(
18268        &self,
18269        sheet: Option<&str>,
18270        row: u32,
18271        col: u32,
18272        current_sheet: &str,
18273    ) -> Result<LiteralValue, ExcelError> {
18274        let sheet_name = sheet.unwrap_or(current_sheet);
18275        if self.graph.sheet_id(sheet_name).is_none() {
18276            return Err(ExcelError::new(ExcelErrorKind::Ref));
18277        }
18278        Ok(self
18279            .get_cell_value(sheet_name, row, col)
18280            .unwrap_or(LiteralValue::Empty))
18281    }
18282
18283    fn resolve_cell_reference_value_formatted(
18284        &self,
18285        sheet: Option<&str>,
18286        row: u32,
18287        col: u32,
18288        current_sheet: &str,
18289    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
18290        // `resolve_cell_reference_value` + `resolve_cell_format` with one
18291        // sheet lookup of each kind.
18292        let sheet_name = sheet.unwrap_or(current_sheet);
18293        let Some(sheet_id) = self.graph.sheet_id(sheet_name) else {
18294            return Err(ExcelError::new(ExcelErrorKind::Ref));
18295        };
18296        let asheet = self.arrow_sheets.sheet(sheet_name);
18297        Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
18298    }
18299
18300    fn build_criteria_mask(
18301        &self,
18302        view: &RangeView<'_>,
18303        col_in_view: usize,
18304        pred: &crate::args::CriteriaPredicate,
18305    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18306        #[cfg(any(test, feature = "test-support"))]
18307        criteria_mask_test_hooks::note_mask(view.dims().0);
18308        if view.dims().1 == 0 {
18309            return None;
18310        }
18311        // If the view is logically open-ended but the backing sheet has no physical rows,
18312        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
18313        let sheet_rows = view.sheet().nrows as usize;
18314        if sheet_rows == 0 || view.start_row() >= sheet_rows {
18315            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
18316        }
18317        compute_criteria_mask(view, col_in_view, pred)
18318    }
18319
18320    fn build_row_visibility_mask(
18321        &self,
18322        view: &RangeView<'_>,
18323        mode: VisibilityMaskMode,
18324    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18325        self.build_row_visibility_mask_for_view(view, mode)
18326    }
18327}
18328
18329impl<R> Engine<R>
18330where
18331    R: EvaluationContext,
18332{
18333    fn clear_spill_projection_and_mirror(
18334        &mut self,
18335        anchor_vertex: VertexId,
18336        delta: Option<&mut DeltaCollector>,
18337    ) {
18338        let spill_cells = self
18339            .graph
18340            .spill_cells_for_anchor(anchor_vertex)
18341            .map(|cells| cells.to_vec())
18342            .unwrap_or_default();
18343        if spill_cells.is_empty() {
18344            return;
18345        }
18346
18347        if let Some(delta) = delta
18348            && delta.mode != DeltaMode::Off
18349        {
18350            let empty = LiteralValue::Empty;
18351            for cell in spill_cells.iter() {
18352                let sheet_name = self.graph.sheet_name(cell.sheet_id);
18353                let old = self
18354                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18355                    .unwrap_or(LiteralValue::Empty);
18356                if old != empty {
18357                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18358                }
18359            }
18360        }
18361
18362        self.graph.clear_spill_region(anchor_vertex);
18363        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
18364            self.record_structural_change(scope);
18365        }
18366
18367        if self.config.arrow_storage_enabled
18368            && self.config.delta_overlay_enabled
18369            && self.config.write_formula_overlay_enabled
18370        {
18371            let empty = LiteralValue::Empty;
18372            for cell in spill_cells.iter() {
18373                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
18374                self.mirror_value_to_computed_overlay(
18375                    &sheet_name,
18376                    cell.coord.row() + 1,
18377                    cell.coord.col() + 1,
18378                    &empty,
18379                );
18380            }
18381        }
18382    }
18383
18384    /// Apply the evaluation outcome for one cyclic SCC: stamp `#CIRC!` on its
18385    /// (optionally filtered) members via `stamp_cycle_error`.
18386    ///
18387    /// This is the single per-SCC application point used by every schedule
18388    /// consumer walking `Schedule::units` (pre-work for #112, where cyclic
18389    /// SCCs will gain runtime verdicts instead of an unconditional stamp).
18390    ///
18391    /// `dirty_filter` preserves the recalc-plan quirk: when `Some(dirty)`,
18392    /// only members present in the set are stamped.
18393    ///
18394    /// Returns the number of vertices stamped (0 when a filter excludes every
18395    /// member), so callers can keep their site-specific `cycle_errors`
18396    /// accounting.
18397    fn apply_cycle_outcome(
18398        &mut self,
18399        cycle: &[VertexId],
18400        mut delta: Option<&mut DeltaCollector>,
18401        dirty_filter: Option<&FxHashSet<VertexId>>,
18402    ) -> usize {
18403        let circ_error = LiteralValue::Error(
18404            ExcelError::new(ExcelErrorKind::Circ)
18405                .with_message("Circular dependency detected".to_string()),
18406        );
18407        let mut stamped = 0usize;
18408        for &vertex_id in cycle {
18409            if let Some(filter) = dirty_filter
18410                && !filter.contains(&vertex_id)
18411            {
18412                continue;
18413            }
18414            self.stamp_cycle_error(vertex_id, &circ_error, delta.as_deref_mut());
18415            stamped += 1;
18416        }
18417        stamped
18418    }
18419
18420    /// Stamp a vertex with `#CIRC!` as part of cycle handling.
18421    ///
18422    /// Unlike a bare `update_vertex_value`, this first tears down any spill the
18423    /// vertex previously anchored: it clears the spilled cells, releases the graph
18424    /// spill registry, drops any lingering region reservation, and mirrors the
18425    /// cleared cells into the computed overlay — the same teardown a normal scalar/
18426    /// error result performs (see `apply_non_array_result_from_parallel` /
18427    /// `clear_spill_projection_and_mirror`). Without this, a #CIRC stamp on a former
18428    /// spill anchor would leave stale spilled values and a reserved region behind
18429    /// (issue #111).
18430    ///
18431    /// When `delta` is provided, the cleared spill cells are recorded (by
18432    /// `clear_spill_projection_and_mirror`) and the anchor's own #CIRC change is
18433    /// recorded here, matching how other result paths emit deltas.
18434    fn stamp_cycle_error(
18435        &mut self,
18436        vertex_id: VertexId,
18437        circ_error: &LiteralValue,
18438        mut delta: Option<&mut DeltaCollector>,
18439    ) {
18440        // Tear down any previous spill projection/region before overwriting the anchor.
18441        if self.graph.spill_registry_has_anchor(vertex_id) {
18442            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
18443        }
18444        // Drop any reservation that was never committed (defensive; normally released
18445        // on the prior successful commit).
18446        self.spill_mgr.release_owner(vertex_id);
18447
18448        // Record the anchor's own #CIRC delta, like other result paths.
18449        if let Some(d) = delta
18450            && d.mode != DeltaMode::Off
18451            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
18452        {
18453            let sheet_name = self.graph.sheet_name(cell.sheet_id);
18454            let old = self
18455                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18456                .unwrap_or(LiteralValue::Empty);
18457            if old != *circ_error {
18458                d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18459            }
18460        }
18461
18462        self.graph.update_vertex_value_ref(vertex_id, circ_error);
18463        self.mirror_vertex_value_to_overlay(vertex_id, circ_error);
18464    }
18465
18466    /// Dispatch point for one `ScheduleUnit::Cycle` (RFC #112, Stage 2).
18467    ///
18468    /// * `CycleDetection::Static` — today's behavior, byte-for-byte: stamp
18469    ///   `#CIRC!` on the (optionally dirty-filtered) members.
18470    /// * `CycleDetection::Runtime` — evaluate the SCC via
18471    ///   [`Self::evaluate_scc_unit`]. The recalc-plan dirty quirk maps to:
18472    ///   no dirty member → skip the task entirely (values stand); any dirty
18473    ///   member → the whole SCC evaluates (an SCC cannot be partially
18474    ///   evaluated).
18475    ///
18476    /// Returns the number of `#CIRC!`-stamped vertices, so call sites can
18477    /// keep their `cycle_errors` accounting (`> 0` ⇒ count the unit).
18478    fn handle_cycle_unit(
18479        &mut self,
18480        cycle: &[VertexId],
18481        mut delta: Option<&mut DeltaCollector>,
18482        dirty_filter: Option<&FxHashSet<VertexId>>,
18483        cancel_flag: Option<&AtomicBool>,
18484    ) -> Result<usize, ExcelError> {
18485        self.resource_checkpoint(cycle.len() as u64)?;
18486        match self.config.cycle.detection {
18487            CycleDetection::Static => {
18488                Ok(self.apply_cycle_outcome(cycle, delta.as_deref_mut(), dirty_filter))
18489            }
18490            CycleDetection::Runtime => {
18491                if let Some(filter) = dirty_filter
18492                    && !cycle.iter().any(|v| filter.contains(v))
18493                {
18494                    return Ok(0);
18495                }
18496                // Both policies share `evaluate_scc_unit`; they differ only
18497                // in the settle loop's live-cycle arm (Error stamps,
18498                // Iterate keeps passing — RFC #113).
18499                self.evaluate_scc_unit(cycle, delta, cancel_flag)
18500            }
18501        }
18502    }
18503
18504    /// Evaluate one statically-cyclic SCC under `CycleDetection::Runtime`
18505    /// (design doc `formualizer-stage2-scc-evaluation-design.md` §3; contract
18506    /// spec §3; Iterate policy arm per RFC #113).
18507    ///
18508    /// Phantom SCCs (live-acyclic) produce ordinary values under both
18509    /// policies; live cycles get `#CIRC!` with live-cycle-only blast radius
18510    /// under `CyclePolicy::Error`, or Excel-style iterative calculation
18511    /// (converge per spec §6 or cap at `max_iterations` passes) under
18512    /// `CyclePolicy::Iterate`. Runs sequentially on the
18513    /// coordinating thread; commits are write-through per member (no
18514    /// `ComputedWriteBuffer` — that buffer is scoped to layer evaluation and
18515    /// always flushed before a Cycle unit runs, G1), so later members' scalar
18516    /// *and* range reads observe earlier members' results through the overlay
18517    /// cascade. Deltas are recorded once per member at end of task (G11).
18518    ///
18519    /// Returns the number of vertices stamped `#CIRC!`.
18520    ///
18521    /// `pub(crate)` so tests can drive SCC shapes (e.g. name-vertex members)
18522    /// that ingest-time cycle rejection makes unreachable via public edits.
18523    pub(crate) fn evaluate_scc_unit(
18524        &mut self,
18525        cycle: &[VertexId],
18526        mut delta: Option<&mut DeltaCollector>,
18527        cancel_flag: Option<&AtomicBool>,
18528    ) -> Result<usize, ExcelError> {
18529        struct SccMember {
18530            vertex: VertexId,
18531            cell: Option<CellRef>,
18532        }
18533
18534        let task_start = crate::instant::FzInstant::now();
18535
18536        // ── 0. Member order (spec §7.13): cells ascending (sheet, row, col);
18537        // name vertices after, lexicographic by folded canonical name; any
18538        // other vertex kind (defensive — `get_evaluation_vertices` only emits
18539        // formula/name kinds) last by id, never evaluated.
18540        let mut cell_members: Vec<(VertexId, CellRef)> = Vec::new();
18541        let mut name_members: Vec<(VertexId, String)> = Vec::new();
18542        let mut other_members: Vec<VertexId> = Vec::new();
18543        for &v in cycle {
18544            match self.graph.get_vertex_kind(v) {
18545                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18546                    match self.graph.get_cell_ref(v) {
18547                        Some(cell) => cell_members.push((v, cell)),
18548                        None => other_members.push(v),
18549                    }
18550                }
18551                VertexKind::NamedScalar | VertexKind::NamedArray => {
18552                    match self.graph.name_key_for_vertex(v) {
18553                        Some(key) => name_members.push((v, key)),
18554                        None => other_members.push(v),
18555                    }
18556                }
18557                _ => other_members.push(v),
18558            }
18559        }
18560        cell_members.sort_unstable_by_key(|(_, c)| (c.sheet_id, c.coord.row(), c.coord.col()));
18561        name_members.sort_unstable_by(|(av, ak), (bv, bk)| ak.cmp(bk).then(av.cmp(bv)));
18562        other_members.sort_unstable();
18563
18564        let cell_refs: Vec<CellRef> = cell_members.iter().map(|(_, c)| *c).collect();
18565        let name_keys: Vec<String> = name_members.iter().map(|(_, k)| k.clone()).collect();
18566        let mut members: Vec<SccMember> = Vec::with_capacity(cycle.len());
18567        for (v, c) in &cell_members {
18568            members.push(SccMember {
18569                vertex: *v,
18570                cell: Some(*c),
18571            });
18572        }
18573        for (v, _) in &name_members {
18574            members.push(SccMember {
18575                vertex: *v,
18576                cell: None,
18577            });
18578        }
18579        for v in &other_members {
18580            members.push(SccMember {
18581                vertex: *v,
18582                cell: None,
18583            });
18584        }
18585        let n = members.len();
18586        // Indices addressable by the collector (cells + names); `other`
18587        // members can be neither edge sources nor targets.
18588        let recordable = cell_refs.len() + name_keys.len();
18589
18590        let circ_error = LiteralValue::Error(
18591            ExcelError::new(ExcelErrorKind::Circ)
18592                .with_message("Circular dependency detected".to_string()),
18593        );
18594
18595        // ── 0b. Spec-§4 persistence repair: structural edits clear computed
18596        // overlays wholesale (`clear_computed_overlay_after_row/_col`), but
18597        // an iterating member's committed value is cycle STATE, not a
18598        // recomputable cache — and in canonical mode the overlay is its ONLY
18599        // home. If the overlay entry vanished since the last recalc, re-seed
18600        // it from the end-of-recalc snapshot (`iterative_state_values`) so
18601        // pass-1 reads (scalar AND range, via the overlay cascade) observe
18602        // the persisted value instead of silently restarting at Empty→0.
18603        // (Found by the iterate edge corpus: inserting/deleting an unrelated
18604        // row reset accumulators, violating spec §4/§7.15.)
18605        if !self.iterative_state_values.is_empty() {
18606            let restore: Vec<(VertexId, LiteralValue)> = members
18607                .iter()
18608                .filter_map(|m| {
18609                    let cell = m.cell?;
18610                    let persisted = self.iterative_state_values.get(&m.vertex)?;
18611                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
18612                    let overlay = self
18613                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18614                        .unwrap_or(LiteralValue::Empty);
18615                    if matches!(overlay, LiteralValue::Empty) {
18616                        Some((m.vertex, persisted.clone()))
18617                    } else {
18618                        None
18619                    }
18620                })
18621                .collect();
18622            for (vertex, value) in restore {
18623                self.mirror_vertex_value_to_overlay(vertex, &value);
18624            }
18625        }
18626
18627        // ── 1. Pre-task value snapshot (overlay-first for cells — G3; the
18628        // graph value map may be evicted in value-cache-disabled mode).
18629        let snapshot: Vec<LiteralValue> = members
18630            .iter()
18631            .map(|m| match m.cell {
18632                Some(cell) => {
18633                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
18634                    self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18635                        .unwrap_or(LiteralValue::Empty)
18636                }
18637                None => self
18638                    .graph
18639                    .get_value(m.vertex)
18640                    .unwrap_or(LiteralValue::Empty),
18641            })
18642            .collect();
18643
18644        // ── 2. Pre-scan: spill anchors (FormulaArray) are stamped `#CIRC!`
18645        // with full spill teardown (spec §7.9, #115) and excluded from
18646        // evaluation. They stay recordable edge TARGETS (readers see `#CIRC!`
18647        // and propagate). Non-evaluable defensive members are excluded too.
18648        let mut excluded = vec![false; n];
18649        let mut last_value = snapshot.clone();
18650        let mut stamped = 0usize;
18651        for (i, m) in members.iter().enumerate() {
18652            match self.graph.get_vertex_kind(m.vertex) {
18653                VertexKind::FormulaArray => {
18654                    // Deltas for the cleared spill-region cells (non-members)
18655                    // can only be recorded here; the anchor's own delta is
18656                    // covered by the end-of-task snapshot comparison (dedup).
18657                    self.stamp_cycle_error(m.vertex, &circ_error, delta.as_deref_mut());
18658                    excluded[i] = true;
18659                    last_value[i] = circ_error.clone();
18660                    stamped += 1;
18661                }
18662                VertexKind::FormulaScalar | VertexKind::NamedScalar | VertexKind::NamedArray => {}
18663                _ => excluded[i] = true,
18664            }
18665        }
18666
18667        let collector = LiveEdgeCollector::new_with_names(&cell_refs, &name_keys);
18668
18669        // Per-member live out-edges, refreshed whenever a member re-runs.
18670        let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
18671        // Position of each member in the most recent pass (-1 = did not run).
18672        let mut pos: Vec<i64> = vec![-1; n];
18673        // Whether each member's committed value changed in the most recent pass.
18674        let mut changed = vec![false; n];
18675
18676        // Evaluate-and-commit one member; returns Ok(true) when the member was
18677        // stamped `#CIRC!` (array result — would-be spill anchor, spec §7.9).
18678        macro_rules! run_member {
18679            ($i:expr) => {{
18680                let i: usize = $i;
18681                let m = &members[i];
18682                if i < recordable {
18683                    collector.set_current(i as u32);
18684                }
18685                let value = {
18686                    let ctx = RecordingContext::new(&*self, &collector);
18687                    match self.evaluate_vertex_recorded(m.vertex, &ctx, &collector) {
18688                        Ok(v) => v,
18689                        Err(e) => LiteralValue::Error(e),
18690                    }
18691                };
18692                let is_cell_formula = m.cell.is_some();
18693                if is_cell_formula && matches!(value, LiteralValue::Array(_)) {
18694                    // A member that *would* spill inside an SCC gets the
18695                    // conservative §7.9 verdict. It has never spilled before
18696                    // (a prior spill would make it FormulaArray, pre-stamped
18697                    // above), so there is no projection to tear down.
18698                    self.stamp_cycle_error(m.vertex, &circ_error, None);
18699                    excluded[i] = true;
18700                    stamped += 1;
18701                    changed[i] = last_value[i] != circ_error;
18702                    last_value[i] = circ_error.clone();
18703                } else {
18704                    self.graph.update_vertex_value_ref(m.vertex, &value);
18705                    self.mirror_vertex_value_to_overlay(m.vertex, &value);
18706                    // §7.14 invariant (G2): a formula member must never be
18707                    // shadowed by a user/delta overlay entry, or iteration
18708                    // reads would silently diverge from committed values.
18709                    #[cfg(debug_assertions)]
18710                    if let Some(cell) = m.cell {
18711                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
18712                        debug_assert!(
18713                            self.read_delta_overlay_cell(
18714                                &sheet_name,
18715                                cell.coord.row() + 1,
18716                                cell.coord.col() + 1
18717                            )
18718                            .is_none(),
18719                            "user overlay must never shadow a formula SCC member ({sheet_name}!r{}c{})",
18720                            cell.coord.row() + 1,
18721                            cell.coord.col() + 1
18722                        );
18723                    }
18724                    changed[i] = last_value[i] != value;
18725                    last_value[i] = value;
18726                }
18727            }};
18728        }
18729
18730        let check_cancel = |flag: Option<&AtomicBool>| -> Result<(), ExcelError> {
18731            if let Some(flag) = flag
18732                && flag.load(Ordering::Relaxed)
18733            {
18734                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18735                    .with_message("Evaluation cancelled during SCC evaluation".to_string()));
18736            }
18737            Ok(())
18738        };
18739
18740        // ── 3. Pass 1: all evaluable members in member order.
18741        check_cancel(cancel_flag)?;
18742        let mut passes = 1usize;
18743        {
18744            let mut p = 0i64;
18745            for i in 0..n {
18746                if excluded[i] {
18747                    continue;
18748                }
18749                run_member!(i);
18750                pos[i] = p;
18751                p += 1;
18752            }
18753        }
18754
18755        // ── 4. Settle loop (design doc §3 step 4; RFC #113 policy arm).
18756        //
18757        // Acyclic classifications settle stale readers exactly (identical
18758        // under both policies — phantom SCCs never iterate). A witnessed
18759        // live cycle dispatches on policy: `Error` stamps `#CIRC!` and
18760        // stops; `Iterate` keeps running full passes over all members in
18761        // member order until converged (spec §6) or capped at
18762        // `max_iterations` total passes. A live cycle that only appears
18763        // mid-settle takes the same arm, and a cycle that dissolves
18764        // mid-iteration falls back to exact acyclic settling.
18765        //
18766        // Defensive acyclic budget: the acyclic settle is monotone, so more
18767        // than |SCC| + 2 settle passes can only be a bug; cap hits stamp the
18768        // remainder and set telemetry. Tracked via `settle_passes` so
18769        // iteration passes (legitimately many) don't consume the budget.
18770        let policy = self.config.cycle.policy;
18771        let cap = n + 2;
18772        let mut witnessed_cycles = 0usize;
18773        let mut capped = false;
18774        // ── Iterate-policy state ──
18775        let mut iterating = false;
18776        let mut converged = false;
18777        let mut exact_fixed_point = false;
18778        // Values committed by the last *full* pass; `None` until the first
18779        // iteration pass runs (pass 1 has no predecessor to compare against)
18780        // and reset when a settle pass runs (no cross-kind comparisons).
18781        let mut prev_pass: Option<Vec<LiteralValue>> = None;
18782        // Final-round convergence stats (overwritten per round so the values
18783        // reported are the ones observed at stop).
18784        let mut iter_max_delta = 0f64;
18785        let mut iter_nan_converged = 0usize;
18786        // Acyclic stale-reader re-eval passes (defensive budget; under pure
18787        // Error flow `1 + settle_passes == passes`, preserving Stage-2
18788        // behavior exactly).
18789        let mut settle_passes = 0usize;
18790        loop {
18791            // Drain this pass's recordings; members that ran replace their
18792            // out-edge set, members that didn't keep last-known edges.
18793            let drained = collector.take_edges();
18794            for i in 0..n {
18795                if pos[i] >= 0 {
18796                    out_edges[i].clear();
18797                }
18798            }
18799            for (from, to) in drained {
18800                debug_assert!(
18801                    pos[from as usize] >= 0,
18802                    "edge from a member that did not run"
18803                );
18804                out_edges[from as usize].push(to);
18805            }
18806            let mut edges: Vec<(u32, u32)> = Vec::new();
18807            for (i, outs) in out_edges.iter().enumerate() {
18808                if excluded[i] {
18809                    continue;
18810                }
18811                for &t in outs {
18812                    edges.push((i as u32, t));
18813                }
18814            }
18815            edges.sort_unstable();
18816            edges.dedup();
18817
18818            let analysis = analyze_live_graph(n, &edges);
18819
18820            if analysis.cycle_count > 0 {
18821                // Classification repeats every iteration pass under
18822                // `Iterate`; record the widest single witness instead of
18823                // accumulating so the count stays "distinct live cycles".
18824                witnessed_cycles = witnessed_cycles.max(analysis.cycle_count);
18825                match policy {
18826                    CyclePolicy::Error => {
18827                        // POLICY (Error): stamp every member of a live cycle,
18828                        // then one settling pass over the remaining members in
18829                        // live-topological order so error propagation
18830                        // downstream is consistent (spec §3.4). Blast radius =
18831                        // live cycles only.
18832                        for i in 0..n {
18833                            if analysis.in_cycle[i] && !excluded[i] {
18834                                self.stamp_cycle_error(members[i].vertex, &circ_error, None);
18835                                excluded[i] = true;
18836                                last_value[i] = circ_error.clone();
18837                                stamped += 1;
18838                            }
18839                        }
18840                        check_cancel(cancel_flag)?;
18841                        let order: Vec<usize> = analysis
18842                            .topo
18843                            .iter()
18844                            .map(|&i| i as usize)
18845                            .filter(|&i| !excluded[i])
18846                            .collect();
18847                        if !order.is_empty() {
18848                            passes += 1;
18849                            for i in order {
18850                                run_member!(i);
18851                            }
18852                        }
18853                        break;
18854                    }
18855                    CyclePolicy::Iterate {
18856                        max_iterations,
18857                        max_change,
18858                    } => {
18859                        // POLICY (Iterate), spec §3.5/§6.
18860                        iterating = true;
18861
18862                        // Convergence test: the full pass that just completed
18863                        // vs the previous full pass, per the spec-§6 rules.
18864                        // `prev_pass` is `None` until an iteration pass has
18865                        // run — pass 1 has no predecessor, so no convergence
18866                        // test occurs before the second pass (spec §6).
18867                        if let Some(prev) = &prev_pass {
18868                            let mut round_max_delta = 0f64;
18869                            let mut round_nan = 0usize;
18870                            let mut all_converged = true;
18871                            let mut round_exact = true;
18872                            for i in 0..n {
18873                                if excluded[i] {
18874                                    // Stamped mid-iteration (array result,
18875                                    // §7.9): the value is pinned and cannot
18876                                    // change again — trivially settled.
18877                                    continue;
18878                                }
18879                                let out = crate::engine::convergence::values_converged(
18880                                    &prev[i],
18881                                    &last_value[i],
18882                                    max_change,
18883                                    self.config.date_system,
18884                                );
18885                                if out.nan_converged {
18886                                    round_nan += 1;
18887                                }
18888                                if let Some(d) = out.abs_delta {
18889                                    round_max_delta = round_max_delta.max(d);
18890                                    if d != 0.0 {
18891                                        round_exact = false;
18892                                    }
18893                                }
18894                                if !out.converged {
18895                                    all_converged = false;
18896                                }
18897                            }
18898                            // Overwrite (not max): telemetry reports the
18899                            // round observed at stop.
18900                            iter_max_delta = round_max_delta;
18901                            iter_nan_converged = round_nan;
18902                            if all_converged {
18903                                exact_fixed_point = round_exact;
18904                                converged = true;
18905                                break;
18906                            }
18907                        }
18908
18909                        // ── Pass-counting reconciliation (spec §6/§7.6):
18910                        // `max_iterations` counts TOTAL passes, pass 1
18911                        // included, and pass 1 has already run by the time a
18912                        // live cycle is first witnessed here. The budget is
18913                        // therefore checked BEFORE evaluating anything more:
18914                        // with `max_iterations: 1` we stop right here — each
18915                        // member was evaluated exactly once this recalc (the
18916                        // Excel accumulator contract) and no convergence test
18917                        // ran (`prev_pass` is still `None`). Capping keeps
18918                        // the last committed values and is NOT an error
18919                        // (Excel parity); telemetry records it.
18920                        if passes >= max_iterations as usize {
18921                            capped = true;
18922                            break;
18923                        }
18924
18925                        check_cancel(cancel_flag)?;
18926                        // One more full pass over every evaluable member in
18927                        // member order (Gauss–Seidel: each commit is visible
18928                        // to later members within the pass). Live edges
18929                        // re-record — guards can flip near convergence
18930                        // (§7.3) — so classification repeats next time
18931                        // around, and a cycle that dissolves drops back to
18932                        // the exact acyclic settle below.
18933                        prev_pass = Some(last_value.clone());
18934                        for x in pos.iter_mut() {
18935                            *x = -1;
18936                        }
18937                        changed.fill(false);
18938                        passes += 1;
18939                        let mut p = 0i64;
18940                        for i in 0..n {
18941                            if excluded[i] {
18942                                continue;
18943                            }
18944                            run_member!(i);
18945                            pos[i] = p;
18946                            p += 1;
18947                        }
18948                        continue;
18949                    }
18950                }
18951            }
18952
18953            // Acyclic: find stale readers — members whose live read of `to`
18954            // happened before `to`'s value changed in the pass that just ran.
18955            let mut stale: Vec<usize> = Vec::new();
18956            for i in 0..n {
18957                if excluded[i] {
18958                    continue;
18959                }
18960                let is_stale = out_edges[i].iter().any(|&t| {
18961                    let t = t as usize;
18962                    changed[t] && (pos[i] < 0 || (pos[t] >= 0 && pos[i] < pos[t]))
18963                });
18964                if is_stale {
18965                    stale.push(i);
18966                }
18967            }
18968            if stale.is_empty() {
18969                break; // values exact — phantom SCC (or dissolved live cycle)
18970            }
18971            if 1 + settle_passes >= cap {
18972                // Defensive only; hitting this is a bug (loud telemetry).
18973                capped = true;
18974                for (i, m) in members.iter().enumerate() {
18975                    if !excluded[i] {
18976                        self.stamp_cycle_error(m.vertex, &circ_error, None);
18977                        excluded[i] = true;
18978                        last_value[i] = circ_error.clone();
18979                        stamped += 1;
18980                    }
18981                }
18982                break;
18983            }
18984
18985            check_cancel(cancel_flag)?;
18986            // Re-evaluate stale readers in live-topo order, recording fresh
18987            // edges (branches may flip on re-eval — spec §7.3 — which is why
18988            // classification repeats).
18989            // A settle pass is a partial sweep: drop the full-pass baseline
18990            // so a live cycle (re)appearing afterwards never compares values
18991            // across mixed pass kinds.
18992            prev_pass = None;
18993            let topo_pos = analysis.topo_positions();
18994            stale.sort_unstable_by_key(|&i| topo_pos[i]);
18995            for x in pos.iter_mut() {
18996                *x = -1;
18997            }
18998            changed.fill(false);
18999            passes += 1;
19000            settle_passes += 1;
19001            for (p, i) in stale.into_iter().enumerate() {
19002                run_member!(i);
19003                pos[i] = p as i64;
19004            }
19005        }
19006
19007        // Iteration that ended because the live cycle dissolved and the
19008        // acyclic settle reached exactness counts as converged (values are
19009        // exact, strictly better than threshold-converged). The defensive
19010        // settle cap (`capped` + stamping) is not.
19011        if iterating && !converged && !capped {
19012            converged = true;
19013        }
19014
19015        // ── 5. End of task: one delta per member whose final value differs
19016        // from the pre-task snapshot (spec §3 side-effect rule, G11).
19017        collector.clear_current();
19018        if let Some(d) = delta
19019            && d.mode != DeltaMode::Off
19020        {
19021            for (i, m) in members.iter().enumerate() {
19022                if let Some(cell) = m.cell
19023                    && last_value[i] != snapshot[i]
19024                {
19025                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19026                }
19027            }
19028        }
19029
19030        // Members of an SCC that iterated re-evaluate on EVERY recalc, like
19031        // Excel's circular cells: register them for the end-of-recalc
19032        // volatile-like redirty (see `pending_iterative_redirty`). Marking
19033        // any one member propagates around the (strongly connected) SCC and
19034        // to downstream dependents, but all members are registered so the
19035        // contract survives partial structural edits between recalcs.
19036        //
19037        // Exception (#368): an SCC that stopped on an
19038        // exact fixed point — every member reproduced its previous value
19039        // bit-for-bit before the pass cap, no NaN identity, no volatile or
19040        // dynamic-reference member — cannot change on a re-run with the same
19041        // inputs, so it is retained clean. The dirty graph decides when it
19042        // runs again. Tolerance-only convergence (|Δ| < max_change but ≠ 0),
19043        // capped SCCs (including the `max_iterations: 1` accumulator
19044        // contract) and volatile cycles keep the per-recalc redirty.
19045        // Whatever the outcome, this task supersedes any earlier retention
19046        // of its members.
19047        if !self.retained_scc_members.is_empty() {
19048            for m in members.iter() {
19049                self.retained_scc_members.remove(&m.vertex);
19050            }
19051        }
19052        if iterating {
19053            let retain = converged
19054                && !capped
19055                && exact_fixed_point
19056                && iter_nan_converged == 0
19057                && members
19058                    .iter()
19059                    .all(|m| !self.graph.is_volatile(m.vertex) && !self.graph.is_dynamic(m.vertex));
19060            if retain {
19061                if self.retained_scc_members.is_empty() {
19062                    self.retained_scc_config_fingerprint = self.retained_scc_config_fingerprint();
19063                    self.retained_scc_function_epoch_seen =
19064                        crate::function_registry::semantic_epoch();
19065                    self.retained_scc_provider_revision_seen =
19066                        self.resolver.planning_semantic_revision();
19067                }
19068                let scc_id = self.next_retained_scc_id;
19069                self.next_retained_scc_id = self.next_retained_scc_id.wrapping_add(1);
19070                for (i, m) in members.iter().enumerate() {
19071                    self.retained_scc_members.insert(m.vertex, scc_id);
19072                    // §4 persistence snapshot, written once: retained
19073                    // members do not pass through `redirty_for_next_recalc`.
19074                    if matches!(last_value[i], LiteralValue::Empty) {
19075                        self.iterative_state_values.remove(&m.vertex);
19076                    } else {
19077                        self.iterative_state_values
19078                            .insert(m.vertex, last_value[i].clone());
19079                    }
19080                }
19081            } else {
19082                self.pending_iterative_redirty
19083                    .extend(members.iter().map(|m| m.vertex));
19084            }
19085        } else if !self.iterative_state_values.is_empty() {
19086            // The cycle dissolved (phantom settle or `#CIRC!` stamping):
19087            // these members are ordinary formulas again and must not carry
19088            // stale iteration state into a future cycle.
19089            for m in members.iter() {
19090                self.iterative_state_values.remove(&m.vertex);
19091            }
19092        }
19093
19094        {
19095            let t = &mut self.last_cycle_telemetry;
19096            t.static_sccs += 1;
19097            if witnessed_cycles == 0 && stamped == 0 && !capped {
19098                t.phantom_sccs += 1;
19099            }
19100            t.live_cycles_witnessed += witnessed_cycles;
19101            t.circ_cells_stamped += stamped;
19102            t.settle_passes_total += passes;
19103            t.max_passes_single_scc = t.max_passes_single_scc.max(passes);
19104            if iterating {
19105                t.iterated_sccs += 1;
19106                if converged {
19107                    t.converged_sccs += 1;
19108                }
19109                t.max_abs_delta_at_stop = t.max_abs_delta_at_stop.max(iter_max_delta);
19110                t.nan_converged += iter_nan_converged;
19111            }
19112            if capped {
19113                t.capped_sccs += 1;
19114            }
19115            t.elapsed_ms += task_start.elapsed().as_millis();
19116        }
19117
19118        Ok(stamped)
19119    }
19120
19121    /// Recorded sibling of [`Self::evaluate_vertex_immutable`]: evaluates one
19122    /// SCC member's AST via an [`Interpreter`] over a [`RecordingContext`] so
19123    /// reads that actually occur are captured as live edges. Value semantics
19124    /// must match `evaluate_vertex_immutable` exactly (including the missing-
19125    /// AST `Number(0.0)` quirk, G14); named Cell/Range/Literal definitions
19126    /// delegate to it after recording the definition region by hand (those
19127    /// reads bypass the context).
19128    fn evaluate_vertex_recorded(
19129        &self,
19130        vertex_id: VertexId,
19131        ctx: &RecordingContext<'_, R>,
19132        collector: &LiveEdgeCollector,
19133    ) -> Result<LiteralValue, ExcelError> {
19134        if !self.graph.vertex_exists(vertex_id) {
19135            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
19136                .with_message(format!("Vertex not found: {vertex_id:?}")));
19137        }
19138
19139        let kind = self.graph.get_vertex_kind(vertex_id);
19140        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
19141
19142        match kind {
19143            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19144                let Some(view) = self.graph.formula_view(vertex_id) else {
19145                    return Ok(LiteralValue::Number(0.0)); // G14 quirk
19146                };
19147                let sheet_name = self.graph.sheet_name(sheet_id);
19148                let cell_ref = self
19149                    .graph
19150                    .get_cell_ref(vertex_id)
19151                    .expect("cell ref for vertex");
19152                let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19153                interpreter
19154                    .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
19155                    .map(|cv| {
19156                        let format = cv.format_id();
19157                        self.record_derived_format(vertex_id, format);
19158                        crate::engine::result_finalization::finalize_formula_result(
19159                            cv.into_literal(),
19160                        )
19161                    })
19162            }
19163            VertexKind::NamedScalar | VertexKind::NamedArray => {
19164                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
19165                    ExcelError::new(ExcelErrorKind::Name)
19166                        .with_message("Named range metadata missing".to_string())
19167                })?;
19168
19169                match &named_range.definition {
19170                    NamedDefinition::Formula { ast, .. } => {
19171                        let context_sheet = match named_range.scope {
19172                            NameScope::Sheet(id) => id,
19173                            NameScope::Workbook => sheet_id,
19174                        };
19175                        let sheet_name = self.graph.sheet_name(context_sheet);
19176                        let cell_ref = self
19177                            .graph
19178                            .get_cell_ref(vertex_id)
19179                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
19180                        let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19181                        if kind == VertexKind::NamedScalar {
19182                            interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
19183                        } else {
19184                            match interpreter.evaluate_ast(ast) {
19185                                Ok(cv) => match cv.into_literal() {
19186                                    v @ LiteralValue::Array(_) => Ok(v),
19187                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
19188                                },
19189                                Err(err) => Ok(LiteralValue::Error(err)),
19190                            }
19191                        }
19192                    }
19193                    NamedDefinition::Cell(cell_ref) => {
19194                        // The definition is read via direct grid access in
19195                        // `evaluate_vertex_immutable`; record the live edge
19196                        // by hand before delegating.
19197                        collector.record_scalar(
19198                            cell_ref.sheet_id,
19199                            cell_ref.coord.row(),
19200                            cell_ref.coord.col(),
19201                        );
19202                        self.evaluate_vertex_immutable(vertex_id)
19203                    }
19204                    NamedDefinition::Range(range_ref) => {
19205                        if range_ref.start.sheet_id == range_ref.end.sheet_id {
19206                            collector.record_rect(
19207                                range_ref.start.sheet_id,
19208                                range_ref.start.coord.row(),
19209                                range_ref.start.coord.col(),
19210                                range_ref.end.coord.row(),
19211                                range_ref.end.coord.col(),
19212                            );
19213                        }
19214                        self.evaluate_vertex_immutable(vertex_id)
19215                    }
19216                    NamedDefinition::Literal(_) => self.evaluate_vertex_immutable(vertex_id),
19217                }
19218            }
19219            _ => self.evaluate_vertex_immutable(vertex_id),
19220        }
19221    }
19222
19223    /// Pending source occupancy is independent of formula preparation and value caches.
19224    fn pending_spill_occupied(&self, anchor: CellRef, end_row: u32, end_col: u32) -> bool {
19225        let sheet = self.graph.sheet_name(anchor.sheet_id);
19226        let package = self
19227            .staged_formulas
19228            .get(sheet)
19229            .and_then(|staged| staged.deferred_package.as_ref());
19230        self.staged_formula_index.occupies_spill(
19231            sheet,
19232            (anchor.coord.row() + 1, anchor.coord.col() + 1),
19233            (end_row + 1, end_col + 1),
19234            |point| package.is_some_and(|package| package.suppressed.contains(&point)),
19235        )
19236    }
19237
19238    fn remember_pending_spill(
19239        &mut self,
19240        vertex: VertexId,
19241        anchor: CellRef,
19242        region: Region,
19243    ) -> Result<(), ExcelError> {
19244        self.cancellation_checkpoint("pending spill occupancy")?;
19245        self.resource_checkpoint(1)?;
19246        if let Some(entry) = self
19247            .blocked_pending_spills
19248            .iter_mut()
19249            .find(|entry| entry.0 == vertex)
19250        {
19251            *entry = (vertex, anchor, region);
19252            return Ok(());
19253        }
19254        if self.blocked_pending_spills.len() == self.blocked_pending_spills.capacity() {
19255            // Geometric growth avoids copying every existing retry entry for
19256            // every new blocked anchor. Admit the entire capacity increment.
19257            let additional = self.blocked_pending_spills.capacity().max(1);
19258            let bytes =
19259                (additional as u64)
19260                    .saturating_mul(std::mem::size_of::<(VertexId, CellRef, Region)>() as u64);
19261            if let Some(ledger) = self.active_resource_ledger.as_mut() {
19262                ledger
19263                    .reserve_retained(bytes)
19264                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19265                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
19266            }
19267            if self
19268                .blocked_pending_spills
19269                .try_reserve_exact(additional)
19270                .is_err()
19271            {
19272                if let Some(ledger) = self.active_resource_ledger.as_mut() {
19273                    ledger
19274                        .release_retained(bytes)
19275                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19276                    self.source_cache_accounted -= bytes;
19277                }
19278                return Err(crate::engine::ResourceLedgerError::Exhausted(
19279                    formualizer_common::ResourceExhaustionDetail {
19280                        reason: formualizer_common::ResourceExhaustionReason::RetainedMemory,
19281                        limit: u64::MAX,
19282                        observed: bytes,
19283                        request_id: None,
19284                    },
19285                )
19286                .into_excel_error());
19287            }
19288        }
19289        self.blocked_pending_spills.push((vertex, anchor, region));
19290        Ok(())
19291    }
19292
19293    // Successful edits wake only intersecting attempted regions. Keep the entry
19294    // until evaluation: logged edits may still roll back, and materializing a
19295    // pending formula must not destroy its anchor's occupancy retry information.
19296    fn invalidate_pending_spills(&mut self, scope: StructuralScope) {
19297        if let StructuralScope::RemovedSheet(sheet) = scope {
19298            self.blocked_pending_spills
19299                .retain(|entry| entry.1.sheet_id != sheet);
19300            return;
19301        }
19302        for &(vertex, anchor, region) in &self.blocked_pending_spills {
19303            let affected = match scope {
19304                StructuralScope::Cell { sheet, row, col } => {
19305                    region.intersects(&Region::point(sheet, row, col))
19306                }
19307                StructuralScope::Region(changed) => region.intersects(&changed),
19308                StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => {
19309                    region.sheet_id() == sheet
19310                }
19311                StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => true,
19312            };
19313            if affected
19314                && self.graph.vertex_exists(vertex)
19315                && self.graph.get_cell_ref(vertex) == Some(anchor)
19316                && matches!(
19317                    self.graph.get_vertex_kind(vertex),
19318                    VertexKind::FormulaScalar | VertexKind::FormulaArray
19319                )
19320            {
19321                self.graph.mark_vertex_dirty(vertex);
19322            }
19323        }
19324    }
19325
19326    fn guard_pending_spill_commit(
19327        &mut self,
19328        anchor_vertex: VertexId,
19329        targets: &[CellRef],
19330    ) -> Result<(), ExcelError> {
19331        let Some(anchor) = self.graph.get_cell_ref(anchor_vertex) else {
19332            return Ok(());
19333        };
19334        let Some(last) = targets.last() else {
19335            return Ok(());
19336        };
19337        let occupied = self.pending_spill_occupied(anchor, last.coord.row(), last.coord.col());
19338        if (occupied
19339            || self
19340                .blocked_pending_spills
19341                .iter()
19342                .any(|entry| entry.0 == anchor_vertex))
19343            && let Err(error) = self.remember_pending_spill(
19344                anchor_vertex,
19345                anchor,
19346                Region::rect(
19347                    anchor.sheet_id,
19348                    anchor.coord.row(),
19349                    last.coord.row(),
19350                    anchor.coord.col(),
19351                    last.coord.col(),
19352                ),
19353            )
19354        {
19355            self.spill_mgr.release_owner(anchor_vertex);
19356            return Err(error);
19357        }
19358        if occupied {
19359            self.spill_mgr.release_owner(anchor_vertex);
19360            return Err(ExcelError::new(ExcelErrorKind::Spill)
19361                .with_message("Spill blocked")
19362                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
19363                    expected_rows: last.coord.row() - anchor.coord.row() + 1,
19364                    expected_cols: last.coord.col() - anchor.coord.col() + 1,
19365                }));
19366        }
19367        Ok(())
19368    }
19369
19370    /// Commit spill via shim and mirror resulting cells into Arrow overlay.
19371    fn commit_spill_and_mirror(
19372        &mut self,
19373        anchor_vertex: VertexId,
19374        targets: &[CellRef],
19375        rows: Vec<Vec<LiteralValue>>,
19376        delta: Option<&mut DeltaCollector>,
19377        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19378    ) -> Result<(), ExcelError> {
19379        self.guard_pending_spill_commit(anchor_vertex, targets)?;
19380        let prev_spill_cells = self
19381            .graph
19382            .spill_cells_for_anchor(anchor_vertex)
19383            .map(|cells| cells.to_vec())
19384            .unwrap_or_default();
19385
19386        if let Some(delta) = delta
19387            && delta.mode != DeltaMode::Off
19388        {
19389            let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
19390                targets.iter().copied().collect();
19391            let empty = LiteralValue::Empty;
19392
19393            // Clears (prev - targets)
19394            for cell in prev_spill_cells.iter() {
19395                if target_set.contains(cell) {
19396                    continue;
19397                }
19398                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19399                let old = self
19400                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19401                    .unwrap_or(LiteralValue::Empty);
19402                if old != empty {
19403                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19404                }
19405            }
19406
19407            // Writes (targets)
19408            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
19409                let width = rows[0].len();
19410                for (idx, cell) in targets.iter().enumerate() {
19411                    let r_off = idx / width;
19412                    let c_off = idx % width;
19413                    let new = rows
19414                        .get(r_off)
19415                        .and_then(|r| r.get(c_off))
19416                        .cloned()
19417                        .unwrap_or(LiteralValue::Empty);
19418                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19419                    let old = self
19420                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19421                        .unwrap_or(LiteralValue::Empty);
19422                    if old != new {
19423                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19424                    }
19425                }
19426            } else {
19427                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
19428                for cell in targets.iter() {
19429                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19430                    let old = self
19431                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19432                        .unwrap_or(LiteralValue::Empty);
19433                    if !matches!(old, LiteralValue::Empty) {
19434                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19435                    }
19436                }
19437            }
19438        }
19439
19440        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
19441        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
19442        let arrow_sheets = &self.arrow_sheets;
19443        self.spill_mgr.commit_array_with_value_probe(
19444            &mut self.graph,
19445            anchor_vertex,
19446            targets,
19447            rows.clone(),
19448            overwritable_formulas,
19449            |g, cell| {
19450                let sheet_name = g.sheet_name(cell.sheet_id);
19451                let asheet = arrow_sheets.sheet(sheet_name)?;
19452                let r0 = cell.coord.row() as usize;
19453                let c0 = cell.coord.col() as usize;
19454                let v = asheet.get_cell_value(r0, c0);
19455                if matches!(v, LiteralValue::Empty) {
19456                    None
19457                } else {
19458                    Some(v)
19459                }
19460            },
19461        )?;
19462
19463        self.blocked_pending_spills
19464            .retain(|entry| entry.0 != anchor_vertex);
19465        if let Some(scope) = Self::structural_scope_from_cells(&prev_spill_cells) {
19466            self.record_structural_change(scope);
19467        }
19468        if let Some(scope) = Self::structural_scope_from_cells(targets) {
19469            self.record_structural_change(scope);
19470        }
19471
19472        if self.config.arrow_storage_enabled
19473            && self.config.delta_overlay_enabled
19474            && self.config.write_formula_overlay_enabled
19475        {
19476            if !prev_spill_cells.is_empty() {
19477                let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
19478                    targets.iter().copied().collect();
19479                let empty = LiteralValue::Empty;
19480                for cell in prev_spill_cells.iter() {
19481                    if !target_set.contains(cell) {
19482                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19483                        self.mirror_value_to_computed_overlay(
19484                            &sheet_name,
19485                            cell.coord.row() + 1,
19486                            cell.coord.col() + 1,
19487                            &empty,
19488                        );
19489                    }
19490                }
19491            }
19492
19493            for (idx, cell) in targets.iter().enumerate() {
19494                if rows.is_empty() || rows[0].is_empty() {
19495                    break;
19496                }
19497                let width = rows[0].len();
19498                let r_off = idx / width;
19499                let c_off = idx % width;
19500                let v = rows[r_off][c_off].clone();
19501                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19502                self.mirror_value_to_computed_overlay(
19503                    &sheet_name,
19504                    cell.coord.row() + 1,
19505                    cell.coord.col() + 1,
19506                    &v,
19507                );
19508            }
19509        }
19510        Ok(())
19511    }
19512}
19513
19514#[cfg(test)]
19515#[path = "tests/authority_schedule_execution.rs"]
19516mod authority_schedule_execution;
19517
19518#[cfg(test)]
19519#[path = "tests/pending_spill.rs"]
19520mod pending_spill_tests;
19521
19522// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
19523//
19524// Compute → Plan → Apply separation for evaluation side-effects.
19525
19526use crate::engine::effects::Effect;
19527use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
19528
19529impl<R> Engine<R>
19530where
19531    R: EvaluationContext,
19532{
19533    /// Plan effects for a single vertex after its value has been computed.
19534    ///
19535    /// This reads graph state but only performs lightweight mutations
19536    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
19537    /// during the planning phase.  Value-changing mutations are deferred to
19538    /// `apply_effect`.
19539    pub(crate) fn plan_vertex_effects(
19540        &mut self,
19541        vertex_id: VertexId,
19542        computed_value: LiteralValue,
19543        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19544    ) -> Result<Vec<Effect>, ExcelError> {
19545        // FR3: a stale dynamic reader publishes nothing; FR2: anything else
19546        // leaves the dirty set at its commit (design §8.2).
19547        if self.freshness_armed() {
19548            if self.freshness_drop_stale(vertex_id) {
19549                return Ok(Vec::new());
19550            }
19551            let effects = self.plan_vertex_effects_unrecorded(
19552                vertex_id,
19553                computed_value,
19554                overwritable_formulas,
19555            )?;
19556            self.freshness_mark_committed(vertex_id);
19557            return Ok(effects);
19558        }
19559        self.plan_vertex_effects_unrecorded(vertex_id, computed_value, overwritable_formulas)
19560    }
19561
19562    fn plan_vertex_effects_unrecorded(
19563        &mut self,
19564        vertex_id: VertexId,
19565        computed_value: LiteralValue,
19566        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19567    ) -> Result<Vec<Effect>, ExcelError> {
19568        let kind = self.graph.get_vertex_kind(vertex_id);
19569        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
19570
19571        // If this vertex's cell is currently covered by a spill from a different
19572        // anchor, ignore the computed result.  Formula vertices are exempt:
19573        // they must still evaluate so that overlapping spills produce #SPILL!.
19574        if !is_formula {
19575            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
19576                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
19577                && owner != vertex_id
19578            {
19579                return Ok(Vec::new());
19580            }
19581            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
19582            return Ok(vec![Effect::WriteCell {
19583                vertex_id,
19584                value: computed_value,
19585            }]);
19586        }
19587
19588        match computed_value {
19589            LiteralValue::Array(rows) => {
19590                self.plan_array_effects(vertex_id, rows, overwritable_formulas)
19591            }
19592            other => self.plan_scalar_effects(vertex_id, other),
19593        }
19594    }
19595
19596    /// Plan effects for a formula vertex that produced a scalar/error result.
19597    fn plan_scalar_effects(
19598        &mut self,
19599        vertex_id: VertexId,
19600        value: LiteralValue,
19601    ) -> Result<Vec<Effect>, ExcelError> {
19602        if !matches!(&value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Spill) {
19603            self.blocked_pending_spills
19604                .retain(|entry| entry.0 != vertex_id);
19605        }
19606        let has_spill = self
19607            .graph
19608            .spill_cells_for_anchor(vertex_id)
19609            .is_some_and(|c| !c.is_empty());
19610
19611        let mut effects = Vec::new();
19612        if has_spill {
19613            effects.push(Effect::SpillClear {
19614                anchor_vertex: vertex_id,
19615            });
19616        }
19617        effects.push(Effect::WriteCell { vertex_id, value });
19618        Ok(effects)
19619    }
19620
19621    /// Plan effects for a formula vertex that produced an array result.
19622    fn plan_array_effects(
19623        &mut self,
19624        vertex_id: VertexId,
19625        rows: Vec<Vec<LiteralValue>>,
19626        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19627    ) -> Result<Vec<Effect>, ExcelError> {
19628        // Lightweight mutation needed for correct spill-blocking checks.
19629        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
19630
19631        let anchor = self
19632            .graph
19633            .get_cell_ref(vertex_id)
19634            .expect("cell ref for vertex");
19635        let sheet_id = anchor.sheet_id;
19636        let h = rows.len() as u32;
19637        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
19638
19639        // Hard cap to avoid vertex explosion from huge dynamic arrays.
19640        let spill_cells = (h as u64).saturating_mul(w as u64);
19641        if spill_cells > self.config.spill.max_spill_cells as u64 {
19642            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
19643        }
19644
19645        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
19646        const PACKED_MAX_ROW: u32 = 1_048_575;
19647        const PACKED_MAX_COL: u32 = 16_383;
19648        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
19649        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
19650        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
19651            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
19652        }
19653
19654        let mut targets = Vec::new();
19655        for r in 0..h {
19656            for c in 0..w {
19657                targets.push(self.graph.make_cell_ref_internal(
19658                    sheet_id,
19659                    anchor.coord.row() + r,
19660                    anchor.coord.col() + c,
19661                ));
19662            }
19663        }
19664
19665        if h != 0 && w != 0 {
19666            let occupied = self.pending_spill_occupied(anchor, end_row, end_col);
19667            if occupied
19668                || self
19669                    .blocked_pending_spills
19670                    .iter()
19671                    .any(|entry| entry.0 == vertex_id)
19672            {
19673                self.spill_mgr.release_owner(vertex_id);
19674                self.remember_pending_spill(
19675                    vertex_id,
19676                    anchor,
19677                    Region::rect(
19678                        sheet_id,
19679                        anchor.coord.row(),
19680                        end_row,
19681                        anchor.coord.col(),
19682                        end_col,
19683                    ),
19684                )?;
19685            }
19686            if occupied {
19687                return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
19688            }
19689        }
19690
19691        // Region lock via spill manager.
19692        match self.spill_mgr.reserve(
19693            vertex_id,
19694            anchor,
19695            SpillShape { rows: h, cols: w },
19696            SpillMeta {
19697                epoch: self.recalc_epoch,
19698                config: self.config.spill,
19699            },
19700        ) {
19701            Ok(()) => {
19702                // Validate spill region is available.
19703                if let Err(_e) = self.graph.plan_spill_region_allowing_formula_overwrite(
19704                    vertex_id,
19705                    &targets,
19706                    overwritable_formulas,
19707                ) {
19708                    return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
19709                }
19710
19711                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
19712                // cell values are not cached in the dependency graph. Consult Arrow storage to
19713                // detect occupied cells in the target region.
19714                if !self.graph.value_cache_enabled() {
19715                    let sheet_name = self.graph.sheet_name(sheet_id);
19716                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
19717                        for cell in targets.iter() {
19718                            // Allow overwriting the anchor itself.
19719                            if *cell == anchor {
19720                                continue;
19721                            }
19722                            // Allow cells already owned by a spill (plan() validated spill ownership).
19723                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
19724                                continue;
19725                            }
19726                            // Skip formula blockers; plan() handled them (or allowed).
19727                            if let Some(vid) = self.graph.get_vertex_id_for_address(cell)
19728                                && vid != vertex_id
19729                            {
19730                                match self.graph.get_vertex_kind(vid) {
19731                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19732                                        continue;
19733                                    }
19734                                    _ => {}
19735                                }
19736                            }
19737
19738                            let v = asheet.get_cell_value(
19739                                cell.coord.row() as usize,
19740                                cell.coord.col() as usize,
19741                            );
19742                            if !matches!(v, LiteralValue::Empty) {
19743                                return self.plan_spill_error_effects(
19744                                    vertex_id,
19745                                    "BlockedByValue",
19746                                    h,
19747                                    w,
19748                                );
19749                            }
19750                        }
19751                    }
19752                }
19753
19754                let top_left = rows
19755                    .first()
19756                    .and_then(|r| r.first())
19757                    .cloned()
19758                    .unwrap_or(LiteralValue::Empty);
19759
19760                let mut effects = Vec::new();
19761                // Clear previous spill if any.
19762                let has_prev = self
19763                    .graph
19764                    .spill_cells_for_anchor(vertex_id)
19765                    .is_some_and(|c| !c.is_empty());
19766                if has_prev {
19767                    effects.push(Effect::SpillClear {
19768                        anchor_vertex: vertex_id,
19769                    });
19770                }
19771                effects.push(Effect::SpillCommit {
19772                    anchor_vertex: vertex_id,
19773                    anchor_cell: anchor,
19774                    target_cells: targets,
19775                    values: rows,
19776                });
19777                effects.push(Effect::WriteCell {
19778                    vertex_id,
19779                    value: top_left,
19780                });
19781                Ok(effects)
19782            }
19783            Err(e) => {
19784                let msg = e.message.unwrap_or_else(|| "Spill blocked".to_string());
19785                self.plan_spill_error_effects(vertex_id, &msg, h, w)
19786            }
19787        }
19788    }
19789
19790    /// Build the effect list for a spill that failed validation.
19791    fn plan_spill_error_effects(
19792        &mut self,
19793        vertex_id: VertexId,
19794        message: &str,
19795        expected_rows: u32,
19796        expected_cols: u32,
19797    ) -> Result<Vec<Effect>, ExcelError> {
19798        self.spill_mgr.release_owner(vertex_id);
19799        let spill_err = ExcelError::new(ExcelErrorKind::Spill)
19800            .with_message(message)
19801            .with_extra(formualizer_common::ExcelErrorExtra::Spill {
19802                expected_rows,
19803                expected_cols,
19804            });
19805        let spill_val = LiteralValue::Error(spill_err);
19806
19807        let effects = vec![
19808            Effect::SpillClear {
19809                anchor_vertex: vertex_id,
19810            },
19811            Effect::WriteCell {
19812                vertex_id,
19813                value: spill_val,
19814            },
19815        ];
19816        Ok(effects)
19817    }
19818
19819    /// Apply a single effect, performing the actual graph mutations.
19820    pub(crate) fn apply_effect(
19821        &mut self,
19822        effect: &Effect,
19823        delta: Option<&mut DeltaCollector>,
19824        log: Option<&mut ChangeLog>,
19825    ) -> Result<(), ExcelError> {
19826        self.apply_effect_with_computed_writes(effect, delta, log, None)
19827    }
19828
19829    fn apply_effect_with_computed_writes(
19830        &mut self,
19831        effect: &Effect,
19832        delta: Option<&mut DeltaCollector>,
19833        log: Option<&mut ChangeLog>,
19834        computed_writes: Option<&mut ComputedWriteBuffer>,
19835    ) -> Result<(), ExcelError> {
19836        match effect {
19837            Effect::WriteCell { vertex_id, value } => {
19838                self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
19839            }
19840            Effect::SpillClear { anchor_vertex } => {
19841                self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
19842            }
19843            Effect::SpillCommit {
19844                anchor_vertex,
19845                anchor_cell: _,
19846                target_cells,
19847                values,
19848            } => {
19849                self.apply_spill_commit(
19850                    *anchor_vertex,
19851                    target_cells,
19852                    values.clone(),
19853                    delta,
19854                    log,
19855                    computed_writes,
19856                )?;
19857            }
19858        }
19859        Ok(())
19860    }
19861
19862    /// Apply a WriteCell effect.
19863    fn apply_write_cell(
19864        &mut self,
19865        vertex_id: VertexId,
19866        value: &LiteralValue,
19867        delta: Option<&mut DeltaCollector>,
19868        mut computed_writes: Option<&mut ComputedWriteBuffer>,
19869    ) -> Result<(), ExcelError> {
19870        if let Some(d) = delta
19871            && d.mode != DeltaMode::Off
19872        {
19873            if let Some(buffer) = computed_writes.as_deref_mut() {
19874                self.flush_computed_write_buffer(buffer)?;
19875            }
19876            if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
19877                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19878                let old = self
19879                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19880                    .unwrap_or(LiteralValue::Empty);
19881                if old != *value {
19882                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19883                }
19884            }
19885        }
19886        self.graph.update_vertex_value_ref(vertex_id, value);
19887        self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)?;
19888        Ok(())
19889    }
19890
19891    /// Apply a SpillClear effect.
19892    fn apply_spill_clear(
19893        &mut self,
19894        anchor_vertex: VertexId,
19895        delta: Option<&mut DeltaCollector>,
19896        log: Option<&mut ChangeLog>,
19897        computed_writes: Option<&mut ComputedWriteBuffer>,
19898    ) -> Result<(), ExcelError> {
19899        if let Some(buffer) = computed_writes {
19900            self.flush_computed_write_buffer(buffer)?;
19901        }
19902
19903        let spill_cells = self
19904            .graph
19905            .spill_cells_for_anchor(anchor_vertex)
19906            .map(|cells| cells.to_vec())
19907            .unwrap_or_default();
19908        if spill_cells.is_empty() {
19909            return Ok(());
19910        }
19911
19912        // Snapshot for ChangeLog before clearing.
19913        let snapshot = if log.is_some() {
19914            self.snapshot_spill_for_anchor(anchor_vertex)
19915        } else {
19916            None
19917        };
19918
19919        // Record delta for cleared cells.
19920        if let Some(d) = delta
19921            && d.mode != DeltaMode::Off
19922        {
19923            let empty = LiteralValue::Empty;
19924            for cell in spill_cells.iter() {
19925                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19926                let old = self
19927                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19928                    .unwrap_or(LiteralValue::Empty);
19929                if old != empty {
19930                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19931                }
19932            }
19933        }
19934
19935        self.graph.clear_spill_region(anchor_vertex);
19936        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
19937            self.record_structural_change(scope);
19938        }
19939
19940        // Mirror Empty to Arrow overlay for cleared cells.
19941        if self.config.arrow_storage_enabled
19942            && self.config.delta_overlay_enabled
19943            && self.config.write_formula_overlay_enabled
19944        {
19945            let empty = LiteralValue::Empty;
19946            for cell in spill_cells.iter() {
19947                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19948                self.mirror_value_to_computed_overlay(
19949                    &sheet_name,
19950                    cell.coord.row() + 1,
19951                    cell.coord.col() + 1,
19952                    &empty,
19953                );
19954            }
19955        }
19956
19957        // ChangeLog.
19958        if let Some(log) = log
19959            && let Some(old) = snapshot
19960        {
19961            log.record(ChangeEvent::SpillCleared {
19962                anchor: anchor_vertex,
19963                old,
19964            });
19965        }
19966        Ok(())
19967    }
19968
19969    /// Apply a SpillCommit effect.
19970    fn apply_spill_commit(
19971        &mut self,
19972        anchor_vertex: VertexId,
19973        target_cells: &[CellRef],
19974        values: Vec<Vec<LiteralValue>>,
19975        delta: Option<&mut DeltaCollector>,
19976        log: Option<&mut ChangeLog>,
19977        computed_writes: Option<&mut ComputedWriteBuffer>,
19978    ) -> Result<(), ExcelError> {
19979        self.guard_pending_spill_commit(anchor_vertex, target_cells)?;
19980        if let Some(buffer) = computed_writes {
19981            self.flush_computed_write_buffer(buffer)?;
19982        }
19983
19984        // Snapshot for ChangeLog before commit.
19985        let old_snapshot = if log.is_some() {
19986            self.snapshot_spill_for_anchor(anchor_vertex)
19987        } else {
19988            None
19989        };
19990
19991        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
19992        self.commit_spill_and_mirror(
19993            anchor_vertex,
19994            target_cells,
19995            values.clone(),
19996            delta,
19997            None, // overwritable_formulas already validated in plan phase
19998        )?;
19999
20000        // ChangeLog.
20001        if let Some(log) = log {
20002            log.record(ChangeEvent::SpillCommitted {
20003                anchor: anchor_vertex,
20004                old: old_snapshot,
20005                new: SpillSnapshot {
20006                    target_cells: target_cells.to_vec(),
20007                    values,
20008                },
20009            });
20010        }
20011        Ok(())
20012    }
20013
20014    /// Snapshot a spill region for ChangeLog recording.
20015    ///
20016    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
20017    /// without creating a `VertexEditor`.
20018    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
20019        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
20020        if cells.is_empty() {
20021            return None;
20022        }
20023
20024        let max = self.config.spill.max_spill_cells as usize;
20025        let mut cells = cells;
20026        if cells.len() > max {
20027            cells.truncate(max);
20028        }
20029
20030        let first = *cells.first().expect("non-empty spill cells");
20031        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
20032        let row0 = first.coord.row();
20033        let col0 = first.coord.col();
20034
20035        let mut max_row = row0;
20036        let mut max_col = col0;
20037        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
20038        for cell in &cells {
20039            max_row = max_row.max(cell.coord.row());
20040            max_col = max_col.max(cell.coord.col());
20041            let v = self
20042                .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20043                .unwrap_or(LiteralValue::Empty);
20044            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
20045        }
20046
20047        let rows = (max_row - row0 + 1) as usize;
20048        let cols = (max_col - col0 + 1) as usize;
20049        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
20050        for r in 0..rows {
20051            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
20052            for c in 0..cols {
20053                row.push(
20054                    by_coord
20055                        .get(&(row0 + r as u32, col0 + c as u32))
20056                        .cloned()
20057                        .unwrap_or(LiteralValue::Empty),
20058                );
20059            }
20060            values.push(row);
20061        }
20062
20063        Some(SpillSnapshot {
20064            target_cells: cells,
20065            values,
20066        })
20067    }
20068
20069    fn flush_before_range_dependent_vertex(
20070        &mut self,
20071        vertex_id: VertexId,
20072        computed_writes: &mut ComputedWriteBuffer,
20073    ) -> Result<(), ExcelError> {
20074        if self.graph.reads_compressed_range(vertex_id) {
20075            self.flush_computed_write_buffer(computed_writes)?;
20076        }
20077        Ok(())
20078    }
20079
20080    fn plan_vertex_effects_with_computed_flush(
20081        &mut self,
20082        vertex_id: VertexId,
20083        computed_value: LiteralValue,
20084        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
20085        computed_writes: &mut ComputedWriteBuffer,
20086    ) -> Result<Vec<Effect>, ExcelError> {
20087        if matches!(&computed_value, LiteralValue::Array(_)) {
20088            self.flush_computed_write_buffer(computed_writes)?;
20089        }
20090        self.plan_vertex_effects(vertex_id, computed_value, overwritable_formulas)
20091    }
20092
20093    // ── Layer evaluation via effects pipeline ──────────────────────────────
20094
20095    fn evaluate_small_layer_direct_effects(
20096        &mut self,
20097        layer: &super::scheduler::Layer,
20098        delta: Option<&mut DeltaCollector>,
20099        log: Option<&mut ChangeLog>,
20100        cancel_flag: Option<&AtomicBool>,
20101        cancel_check_every: usize,
20102        cancel_message: &'static str,
20103    ) -> Result<usize, ExcelError> {
20104        let cancel = cancel_flag.map(|flag| (flag, cancel_check_every, cancel_message));
20105        self.evaluate_layer_units(layer, delta, log, cancel, false)
20106    }
20107
20108    /// Sequential layer walk over its units (single cells and family runs):
20109    /// each unit evaluates, then its vertices' effects apply in order. With
20110    /// `buffered`, computed writes coalesce in a layer buffer (flushed before
20111    /// a unit that reads a compressed range, before array results, and at
20112    /// the end); otherwise they apply directly. `cancel` = (flag, check every
20113    /// N vertices, message).
20114    fn evaluate_layer_units(
20115        &mut self,
20116        layer: &super::scheduler::Layer,
20117        delta: Option<&mut DeltaCollector>,
20118        log: Option<&mut ChangeLog>,
20119        cancel: Option<(&AtomicBool, usize, &'static str)>,
20120        buffered: bool,
20121    ) -> Result<usize, ExcelError> {
20122        self.evaluate_layer_units_until(layer, delta, log, cancel, buffered, None)
20123    }
20124
20125    /// [`Self::evaluate_layer_units`] that stops before the next unit once
20126    /// `stop_at` has passed; returns the vertices evaluated (a prefix of
20127    /// the layer, all committed).
20128    fn evaluate_layer_units_until(
20129        &mut self,
20130        layer: &super::scheduler::Layer,
20131        mut delta: Option<&mut DeltaCollector>,
20132        mut log: Option<&mut ChangeLog>,
20133        cancel: Option<(&AtomicBool, usize, &'static str)>,
20134        buffered: bool,
20135        stop_at: Option<crate::instant::FzInstant>,
20136    ) -> Result<usize, ExcelError> {
20137        // A chain unit: its run through the chain lift, or else cell by
20138        // cell in row order, each written before the next reads it.
20139        let mut chain_values = None;
20140        if layer.sequential && !layer.runs.is_empty() {
20141            chain_values = match layer.runs.as_slice() {
20142                [run] if run.start == 0 && run.len as usize == layer.vertices.len() => {
20143                    self.try_chain_lift(*run, &layer.vertices)
20144                }
20145                _ => None,
20146            };
20147            if chain_values.is_none() {
20148                let cells = super::scheduler::Layer {
20149                    vertices: layer.vertices.clone(),
20150                    runs: Vec::new(),
20151                    sequential: true,
20152                };
20153                return self.evaluate_layer_units_until(&cells, delta, log, cancel, false, stop_at);
20154            }
20155        }
20156        let chained = chain_values.is_some();
20157        // The chain lift computed every member: one block write, as a run.
20158        let buffered = buffered || chained;
20159        // A dynamic reader's targets are not always ordered before it (its
20160        // pre-probe or observed reads can miss them, e.g. after a structural
20161        // edit). In a buffered layer a member's dirty flag is cleared at its
20162        // commit but its value written at the flush: the members committed
20163        // since the last flush count as dirty for the reader's freshness
20164        // check, which then re-plans it after them.
20165        let track_unflushed = buffered
20166            && self.freshness_armed()
20167            && layer.vertices.iter().any(|&v| self.graph.is_dynamic(v));
20168        let mut committed_unit: &[VertexId] = &[];
20169        let mut computed_writes = ComputedWriteBuffer::default();
20170        let mut next_check = 0usize;
20171        let mut done = 0usize;
20172        for unit in layer_units(layer) {
20173            if done > 0
20174                && let Some(stop_at) = stop_at
20175                && crate::instant::FzInstant::now() >= stop_at
20176            {
20177                self.flush_computed_write_buffer(&mut computed_writes)?;
20178                if track_unflushed {
20179                    self.freshness_flushed();
20180                }
20181                return Ok(done);
20182            }
20183            if let Some((flag, every, message)) = cancel
20184                && every > 0
20185                && done >= next_check
20186            {
20187                next_check = (done / every + 1) * every;
20188                if flag.load(Ordering::Relaxed) {
20189                    if buffered {
20190                        self.flush_computed_write_buffer(&mut computed_writes)?;
20191                    }
20192                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
20193                        .with_message(message.to_string()));
20194                }
20195            }
20196            if buffered && self.unit_reads_compressed_range(layer, unit) {
20197                self.flush_computed_write_buffer(&mut computed_writes)?;
20198            }
20199            // The previous unit's members are committed (dirty flags
20200            // cleared); while their values wait in the buffer, a dynamic
20201            // reader's read of them is stale (`freshness_dirty_reads`).
20202            if track_unflushed {
20203                if computed_writes.is_empty() {
20204                    self.freshness_flushed();
20205                } else {
20206                    self.freshness_note_unflushed(committed_unit);
20207                }
20208                committed_unit = unit_members(layer, unit);
20209            }
20210            let values = match (chain_values.take(), unit) {
20211                (Some(chain), LayerUnit::Run(run)) => {
20212                    let members =
20213                        &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20214                    let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20215                    match self.commit_run_numbers(
20216                        run,
20217                        members,
20218                        &chain,
20219                        delta_active,
20220                        Some(&mut computed_writes),
20221                    ) {
20222                        Ok(true) => {
20223                            done += chain.len();
20224                            continue;
20225                        }
20226                        Ok(false) => {}
20227                        Err(e) => {
20228                            self.flush_computed_write_buffer(&mut computed_writes)?;
20229                            return Err(e);
20230                        }
20231                    }
20232                    members
20233                        .iter()
20234                        .copied()
20235                        .zip(chain.into_iter().map(LiteralValue::Number))
20236                        .collect()
20237                }
20238                (_, unit) => self.evaluate_unit_immutable(layer, unit),
20239            };
20240            done += values.len();
20241            if let LayerUnit::Run(run) = unit {
20242                let members = &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20243                let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20244                let committed = self.commit_run_scalars(
20245                    run,
20246                    members,
20247                    &values,
20248                    delta_active,
20249                    buffered.then_some(&mut computed_writes),
20250                );
20251                match committed {
20252                    Ok(true) => continue,
20253                    Ok(false) => {}
20254                    Err(e) => {
20255                        self.flush_computed_write_buffer(&mut computed_writes)?;
20256                        return Err(e);
20257                    }
20258                }
20259            }
20260            for (vertex_id, value) in values {
20261                let effects = if buffered {
20262                    self.plan_vertex_effects_with_computed_flush(
20263                        vertex_id,
20264                        value,
20265                        None,
20266                        &mut computed_writes,
20267                    )
20268                } else {
20269                    self.plan_vertex_effects(vertex_id, value, None)
20270                };
20271                let effects = match effects {
20272                    Ok(effects) => effects,
20273                    Err(e) => {
20274                        self.flush_computed_write_buffer(&mut computed_writes)?;
20275                        return Err(e);
20276                    }
20277                };
20278                for effect in &effects {
20279                    if let Err(e) = self.apply_effect_with_computed_writes(
20280                        effect,
20281                        delta.as_deref_mut(),
20282                        log.as_deref_mut(),
20283                        buffered.then_some(&mut computed_writes),
20284                    ) {
20285                        self.flush_computed_write_buffer(&mut computed_writes)?;
20286                        return Err(e);
20287                    }
20288                }
20289            }
20290        }
20291        self.flush_computed_write_buffer(&mut computed_writes)?;
20292        if track_unflushed {
20293            self.freshness_flushed();
20294        }
20295        // Debug builds: every chain member equals the per-cell path, now
20296        // that the members above it are written.
20297        #[cfg(debug_assertions)]
20298        if chained {
20299            for &v in &layer.vertices {
20300                let cell = self.graph.get_cell_ref(v);
20301                let (sheet, row, col) = cell
20302                    .map(|c| {
20303                        (
20304                            self.graph.sheet_name(c.sheet_id).to_string(),
20305                            c.coord.row() + 1,
20306                            c.coord.col() + 1,
20307                        )
20308                    })
20309                    .expect("chain member cell");
20310                let written = self.get_cell_value(&sheet, row, col);
20311                let oracle = self
20312                    .evaluate_vertex_immutable(v)
20313                    .unwrap_or_else(LiteralValue::Error);
20314                assert!(
20315                    written
20316                        .as_ref()
20317                        .is_some_and(|w| same_value_bits(w, &oracle)),
20318                    "chain member {sheet}!R{row}C{col}: {written:?} vs per-cell {oracle:?}"
20319                );
20320            }
20321        }
20322        #[cfg(not(debug_assertions))]
20323        let _ = chained;
20324        Ok(layer.vertices.len())
20325    }
20326
20327    /// Evaluate a layer sequentially using the effects pipeline.
20328    fn evaluate_layer_sequential_effects(
20329        &mut self,
20330        layer: &super::scheduler::Layer,
20331    ) -> Result<usize, ExcelError> {
20332        let buffered = buffer_layer_writes(layer);
20333        self.evaluate_layer_units(layer, None, None, None, buffered)
20334    }
20335
20336    /// Evaluate a layer sequentially with delta collection via effects pipeline.
20337    fn evaluate_layer_sequential_with_delta_effects(
20338        &mut self,
20339        layer: &super::scheduler::Layer,
20340        delta: &mut DeltaCollector,
20341    ) -> Result<usize, ExcelError> {
20342        let buffered = buffer_layer_writes(layer);
20343        self.evaluate_layer_units(layer, Some(delta), None, None, buffered)
20344    }
20345
20346    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
20347    fn evaluate_layer_sequential_cancellable_effects(
20348        &mut self,
20349        layer: &super::scheduler::Layer,
20350        cancel_flag: &AtomicBool,
20351    ) -> Result<usize, ExcelError> {
20352        let buffered = buffer_layer_writes(layer);
20353        let cancel = (cancel_flag, 256, "Evaluation cancelled within layer");
20354        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
20355    }
20356
20357    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
20358    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
20359        &mut self,
20360        layer: &super::scheduler::Layer,
20361        cancel_flag: &AtomicBool,
20362    ) -> Result<usize, ExcelError> {
20363        let buffered = buffer_layer_writes(layer);
20364        let cancel = (
20365            cancel_flag,
20366            128,
20367            "Demand-driven evaluation cancelled within layer",
20368        );
20369        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
20370    }
20371
20372    /// Evaluate a layer in parallel, applying via effects pipeline.
20373    fn evaluate_layer_parallel_effects(
20374        &mut self,
20375        layer: &super::scheduler::Layer,
20376        min_chunk: u32,
20377    ) -> Result<usize, ExcelError> {
20378        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20379
20380        let phases = self.parallel_phases(layer);
20381
20382        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20383        let mut applied = 0usize;
20384
20385        for (units, group) in &phases {
20386            let group = &group[..];
20387            if group.is_empty() {
20388                continue;
20389            }
20390            let mut computed_writes = ComputedWriteBuffer::default();
20391
20392            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
20393                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, min_chunk));
20394
20395            // FR3: a parallel group is one commit unit; one stale reader
20396            // drops the whole group (it stays dirty and replans).
20397            self.freshness_gate_group(group);
20398            match results {
20399                Ok(vertex_results) => {
20400                    let (vertex_results, committed) = self.commit_parallel_runs(
20401                        layer,
20402                        units,
20403                        vertex_results,
20404                        false,
20405                        &mut computed_writes,
20406                    )?;
20407                    applied = applied.saturating_add(committed);
20408                    // Arrays first, then scalars — establishes spill regions before
20409                    // scalar results that might land inside a spilled region.
20410                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20411                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20412                    for (vertex_id, result) in vertex_results {
20413                        if matches!(result, LiteralValue::Array(_)) {
20414                            arrays.push((vertex_id, result));
20415                        } else {
20416                            others.push((vertex_id, result));
20417                        }
20418                    }
20419                    for (vertex_id, result) in arrays {
20420                        let effects = match self.plan_vertex_effects_with_computed_flush(
20421                            vertex_id,
20422                            result,
20423                            Some(&inflight),
20424                            &mut computed_writes,
20425                        ) {
20426                            Ok(effects) => effects,
20427                            Err(e) => {
20428                                self.flush_computed_write_buffer(&mut computed_writes)?;
20429                                return Err(e);
20430                            }
20431                        };
20432                        for effect in &effects {
20433                            if let Err(e) = self.apply_effect_with_computed_writes(
20434                                effect,
20435                                None,
20436                                None,
20437                                Some(&mut computed_writes),
20438                            ) {
20439                                self.flush_computed_write_buffer(&mut computed_writes)?;
20440                                return Err(e);
20441                            }
20442                        }
20443                        applied = applied.saturating_add(1);
20444                    }
20445                    // Make all array spill/top-left writes visible before scalar effects in this group.
20446                    self.flush_computed_write_buffer(&mut computed_writes)?;
20447                    for (vertex_id, result) in others {
20448                        let effects = match self.plan_vertex_effects_with_computed_flush(
20449                            vertex_id,
20450                            result,
20451                            Some(&inflight),
20452                            &mut computed_writes,
20453                        ) {
20454                            Ok(effects) => effects,
20455                            Err(e) => {
20456                                self.flush_computed_write_buffer(&mut computed_writes)?;
20457                                return Err(e);
20458                            }
20459                        };
20460                        for effect in &effects {
20461                            if let Err(e) = self.apply_effect_with_computed_writes(
20462                                effect,
20463                                None,
20464                                None,
20465                                Some(&mut computed_writes),
20466                            ) {
20467                                self.flush_computed_write_buffer(&mut computed_writes)?;
20468                                return Err(e);
20469                            }
20470                        }
20471                        applied = applied.saturating_add(1);
20472                    }
20473                    // Flush at the group boundary; phase1 must be visible before phase2.
20474                    self.flush_computed_write_buffer(&mut computed_writes)?;
20475                }
20476                Err(e) => {
20477                    self.flush_computed_write_buffer(&mut computed_writes)?;
20478                    return Err(e);
20479                }
20480            }
20481        }
20482
20483        Ok(applied)
20484    }
20485
20486    /// Evaluate a layer in parallel with delta collection via effects pipeline.
20487    fn evaluate_layer_parallel_with_delta_effects(
20488        &mut self,
20489        layer: &super::scheduler::Layer,
20490        delta: &mut DeltaCollector,
20491    ) -> Result<usize, ExcelError> {
20492        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20493
20494        let phases = self.parallel_phases(layer);
20495
20496        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20497        let mut applied = 0usize;
20498
20499        for (units, group) in &phases {
20500            let group = &group[..];
20501            if group.is_empty() {
20502                continue;
20503            }
20504            let mut computed_writes = ComputedWriteBuffer::default();
20505            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
20506                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, 8));
20507
20508            // FR3: a parallel group is one commit unit; one stale reader
20509            // drops the whole group (it stays dirty and replans).
20510            self.freshness_gate_group(group);
20511            match results {
20512                Ok(vertex_results) => {
20513                    let (vertex_results, committed) = self.commit_parallel_runs(
20514                        layer,
20515                        units,
20516                        vertex_results,
20517                        delta.mode != DeltaMode::Off,
20518                        &mut computed_writes,
20519                    )?;
20520                    applied = applied.saturating_add(committed);
20521                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20522                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20523                    for (vertex_id, result) in vertex_results {
20524                        if matches!(result, LiteralValue::Array(_)) {
20525                            arrays.push((vertex_id, result));
20526                        } else {
20527                            others.push((vertex_id, result));
20528                        }
20529                    }
20530                    for (vertex_id, result) in arrays {
20531                        let effects = match self.plan_vertex_effects_with_computed_flush(
20532                            vertex_id,
20533                            result,
20534                            Some(&inflight),
20535                            &mut computed_writes,
20536                        ) {
20537                            Ok(effects) => effects,
20538                            Err(e) => {
20539                                self.flush_computed_write_buffer(&mut computed_writes)?;
20540                                return Err(e);
20541                            }
20542                        };
20543                        for effect in &effects {
20544                            if let Err(e) = self.apply_effect_with_computed_writes(
20545                                effect,
20546                                Some(delta),
20547                                None,
20548                                Some(&mut computed_writes),
20549                            ) {
20550                                self.flush_computed_write_buffer(&mut computed_writes)?;
20551                                return Err(e);
20552                            }
20553                        }
20554                        applied = applied.saturating_add(1);
20555                    }
20556                    self.flush_computed_write_buffer(&mut computed_writes)?;
20557                    for (vertex_id, result) in others {
20558                        let effects = match self.plan_vertex_effects_with_computed_flush(
20559                            vertex_id,
20560                            result,
20561                            Some(&inflight),
20562                            &mut computed_writes,
20563                        ) {
20564                            Ok(effects) => effects,
20565                            Err(e) => {
20566                                self.flush_computed_write_buffer(&mut computed_writes)?;
20567                                return Err(e);
20568                            }
20569                        };
20570                        for effect in &effects {
20571                            if let Err(e) = self.apply_effect_with_computed_writes(
20572                                effect,
20573                                Some(delta),
20574                                None,
20575                                Some(&mut computed_writes),
20576                            ) {
20577                                self.flush_computed_write_buffer(&mut computed_writes)?;
20578                                return Err(e);
20579                            }
20580                        }
20581                        applied = applied.saturating_add(1);
20582                    }
20583                    self.flush_computed_write_buffer(&mut computed_writes)?;
20584                }
20585                Err(e) => {
20586                    self.flush_computed_write_buffer(&mut computed_writes)?;
20587                    return Err(e);
20588                }
20589            }
20590        }
20591
20592        Ok(applied)
20593    }
20594
20595    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
20596    fn evaluate_layer_parallel_cancellable_effects(
20597        &mut self,
20598        layer: &super::scheduler::Layer,
20599        cancel_flag: &AtomicBool,
20600    ) -> Result<usize, ExcelError> {
20601        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20602
20603        if cancel_flag.load(Ordering::Relaxed) {
20604            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
20605                .with_message("Parallel evaluation cancelled before starting".to_string()));
20606        }
20607
20608        let phases = self.parallel_phases(layer);
20609
20610        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20611        let mut applied = 0usize;
20612
20613        for (units, group) in &phases {
20614            let group = &group[..];
20615            if group.is_empty() {
20616                continue;
20617            }
20618            let mut computed_writes = ComputedWriteBuffer::default();
20619
20620            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> = thread_pool
20621                .install(|| self.evaluate_units_parallel(layer, units, Some(cancel_flag), 8));
20622
20623            // FR3: a parallel group is one commit unit; one stale reader
20624            // drops the whole group (it stays dirty and replans).
20625            self.freshness_gate_group(group);
20626            match results {
20627                Ok(vertex_results) => {
20628                    let (vertex_results, committed) = self.commit_parallel_runs(
20629                        layer,
20630                        units,
20631                        vertex_results,
20632                        false,
20633                        &mut computed_writes,
20634                    )?;
20635                    applied = applied.saturating_add(committed);
20636                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20637                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20638                    for (vertex_id, result) in vertex_results {
20639                        if matches!(result, LiteralValue::Array(_)) {
20640                            arrays.push((vertex_id, result));
20641                        } else {
20642                            others.push((vertex_id, result));
20643                        }
20644                    }
20645                    for (vertex_id, result) in arrays {
20646                        let effects = match self.plan_vertex_effects_with_computed_flush(
20647                            vertex_id,
20648                            result,
20649                            Some(&inflight),
20650                            &mut computed_writes,
20651                        ) {
20652                            Ok(effects) => effects,
20653                            Err(e) => {
20654                                self.flush_computed_write_buffer(&mut computed_writes)?;
20655                                return Err(e);
20656                            }
20657                        };
20658                        for effect in &effects {
20659                            if let Err(e) = self.apply_effect_with_computed_writes(
20660                                effect,
20661                                None,
20662                                None,
20663                                Some(&mut computed_writes),
20664                            ) {
20665                                self.flush_computed_write_buffer(&mut computed_writes)?;
20666                                return Err(e);
20667                            }
20668                        }
20669                        applied = applied.saturating_add(1);
20670                    }
20671                    self.flush_computed_write_buffer(&mut computed_writes)?;
20672                    for (vertex_id, result) in others {
20673                        let effects = match self.plan_vertex_effects_with_computed_flush(
20674                            vertex_id,
20675                            result,
20676                            Some(&inflight),
20677                            &mut computed_writes,
20678                        ) {
20679                            Ok(effects) => effects,
20680                            Err(e) => {
20681                                self.flush_computed_write_buffer(&mut computed_writes)?;
20682                                return Err(e);
20683                            }
20684                        };
20685                        for effect in &effects {
20686                            if let Err(e) = self.apply_effect_with_computed_writes(
20687                                effect,
20688                                None,
20689                                None,
20690                                Some(&mut computed_writes),
20691                            ) {
20692                                self.flush_computed_write_buffer(&mut computed_writes)?;
20693                                return Err(e);
20694                            }
20695                        }
20696                        applied = applied.saturating_add(1);
20697                    }
20698                    self.flush_computed_write_buffer(&mut computed_writes)?;
20699                }
20700                Err(e) => {
20701                    self.flush_computed_write_buffer(&mut computed_writes)?;
20702                    return Err(e);
20703                }
20704            }
20705        }
20706
20707        Ok(applied)
20708    }
20709
20710    // ── Top-level evaluate_all_logged ───────────────────────────────────────
20711
20712    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
20713    ///
20714    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
20715    /// every effect application so that spill commits/clears are captured.
20716    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
20717        self.observe_evaluation_resource_request(EvaluationRequestKind::FullLogged, |engine| {
20718            engine.evaluate_all_logged_unobserved(log)
20719        })
20720    }
20721
20722    fn evaluate_all_logged_unobserved(
20723        &mut self,
20724        log: &mut ChangeLog,
20725    ) -> Result<EvalResult, ExcelError> {
20726        self.observe_function_semantic_epoch()?;
20727        let _source_cache = self.source_cache_session();
20728        self.validate_deterministic_mode()?;
20729        if self.config.defer_graph_building {
20730            self.build_graph_all()?;
20731        }
20732        self.require_unified_authority()?;
20733        self.begin_evaluation_request();
20734        self.reset_virtual_dep_telemetry_if_disabled();
20735        let start = crate::instant::FzInstant::now();
20736        let mut computed_vertices = 0;
20737        let mut cycle_errors = 0;
20738
20739        let mut replan_iterations = 0;
20740        const MAX_REPLAN: usize = 5;
20741        let mut telemetry = self
20742            .config
20743            .enable_virtual_dep_telemetry
20744            .then(|| self.start_virtual_dep_telemetry());
20745
20746        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
20747
20748        let result = (|| -> Result<EvalResult, ExcelError> {
20749            loop {
20750                let to_evaluate = self.graph.get_evaluation_vertices();
20751                if to_evaluate.is_empty() {
20752                    if let Some(t) = telemetry.as_mut()
20753                        && t.bailout_reason.is_none()
20754                    {
20755                        t.bailout_reason = Some("no_work");
20756                    }
20757                    break;
20758                }
20759
20760                let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
20761                if let Some(t) = telemetry.as_mut() {
20762                    Self::accumulate_schedule_meta(t, &meta);
20763                }
20764
20765                // Walk units in condensation order: stamp cycles at their
20766                // position, evaluate layers with ChangeLog recording.
20767                self.begin_pass(&schedule);
20768                for (unit_index, &unit) in schedule.units.iter().enumerate() {
20769                    match unit {
20770                        ScheduleUnit::Cycle(i) => {
20771                            // Journal integration (design doc §4 last row): the
20772                            // ChangeLog in this path only records SpillClear /
20773                            // SpillCommit events; WriteCell effects are never
20774                            // logged (see `apply_write_cell`). Runtime SCC tasks
20775                            // write values directly and never spill (§7.9 stamps
20776                            // would-be anchors), and their spill *teardown* is the
20777                            // same unlogged `stamp_cycle_error` the Static path
20778                            // already uses here — so direct commits coexist with
20779                            // the journal cleanly, with identical semantics to
20780                            // Static. Pinned by `scc_runtime_cycles` tests.
20781                            if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0
20782                            {
20783                                cycle_errors += 1;
20784                            }
20785                        }
20786                        ScheduleUnit::Layer(i) => {
20787                            computed_vertices +=
20788                                self.evaluate_layer_logged(schedule.unit_layer(i), log)?;
20789                        }
20790                    }
20791                    if self.stop_after_unit(&schedule, unit_index) {
20792                        break;
20793                    }
20794                }
20795
20796                let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
20797                if let Some(t) = telemetry.as_mut() {
20798                    t.changed_vdeps_total += changed_vertices.len();
20799                }
20800                self.resource_checkpoint(0)?;
20801                if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
20802                    if let Some(t) = telemetry.as_mut() {
20803                        t.bailout_reason = Some("converged");
20804                    }
20805                    break;
20806                }
20807                if replan_iterations >= MAX_REPLAN {
20808                    if let Some(mut t) = telemetry.take() {
20809                        t.bailout_reason = Some("max_replan");
20810                        t.replan_iterations = replan_iterations;
20811                        self.last_virtual_dep_telemetry = t;
20812                    }
20813                    return Err(
20814                        self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
20815                    );
20816                }
20817                replan_iterations += 1;
20818            }
20819
20820            if let Some(mut t) = telemetry {
20821                t.replan_iterations = replan_iterations;
20822                self.last_virtual_dep_telemetry = t;
20823            }
20824
20825            self.redirty_for_next_recalc();
20826            self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
20827
20828            Ok(EvalResult {
20829                computed_vertices,
20830                cycle_errors,
20831                elapsed: start.elapsed(),
20832            })
20833        })();
20834        log.end_compound();
20835        result
20836    }
20837
20838    /// Evaluate a single layer with ChangeLog recording.
20839    fn evaluate_layer_logged(
20840        &mut self,
20841        layer: &super::scheduler::Layer,
20842        log: &mut ChangeLog,
20843    ) -> Result<usize, ExcelError> {
20844        self.resource_checkpoint(layer.vertices.len() as u64)?;
20845        self.evaluate_layer_units(layer, None, Some(log), None, true)
20846    }
20847}