Skip to main content

formualizer_eval/engine/
eval.rs

1use crate::SheetId;
2
3mod criteria;
4mod exact;
5mod family;
6mod kernels;
7mod lift;
8mod memo;
9use crate::arrow_store::{OverlayFragment, OverlayValue, SheetStore};
10#[cfg(test)]
11use crate::engine::Scheduler;
12use crate::engine::arena::AstNodeId;
13use crate::engine::eval_delta::{
14    DeltaCollector, DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy,
15};
16use crate::engine::graph::editor::change_log::MutationCapture;
17use crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan;
18use crate::engine::ingest_pipeline::{DependencyPlanRow, FormulaAstInput};
19use crate::engine::live_edges::{LiveEdgeCollector, RecordingContext};
20use crate::engine::live_graph::analyze_live_graph;
21use crate::engine::lookup_index_cache::{
22    BuildOutcome, LookupAxis, LookupIndex, LookupIndexCache, LookupIndexCacheReport,
23    LookupIndexKey, estimate_bytes,
24};
25use crate::engine::named_range::{NameScope, NamedDefinition};
26use crate::engine::range_view::RangeView;
27use crate::engine::row_visibility::RowVisibilityState;
28use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
29use crate::engine::target_preparation::{
30    StagedFormulaIndex, StagedFormulaLease, StagedPackageLease,
31};
32use crate::engine::used_extent::{
33    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
34};
35use crate::engine::virtual_deps::VirtualDepBuilder;
36use family::{LayerUnit, layer_units, unit_members};
37
38#[path = "freshness.rs"]
39mod freshness;
40use crate::engine::template::region::Region;
41use crate::engine::{
42    ChangeLogger, CycleDetection, CyclePolicy, DependencyGraph, EvalConfig, EvaluationRequestKind,
43    EvaluationRequestOutcome, EvaluationResourceBaselineStats, EvaluationResourceRequestStats,
44    FormulaDirtyLeaseOutcome, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
45    FormulaParseDiagnostic, FormulaParsePolicy, FormulaPlaneMode, ResourceLedger,
46    RowVisibilitySource, ScheduleUnit, VertexId, VertexKind, VisibilityMaskMode,
47};
48use crate::function::FnCaps;
49use crate::interpreter::Interpreter;
50use crate::reference::{CellRef, Coord, RangeRef};
51use crate::traits::FunctionProvider;
52use crate::traits::{EvaluationContext, ReferenceInfo, Resolver};
53use formualizer_common::{
54    CoordBuildHasher, LiteralValue, col_letters_from_1based, parse_a1_1based,
55};
56use formualizer_parse::parser::ReferenceType;
57use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind};
58use rayon::ThreadPoolBuilder;
59use rustc_hash::{FxHashMap, FxHashSet};
60use std::collections::{BTreeMap, BTreeSet, VecDeque};
61use std::sync::Arc;
62use std::sync::atomic::{AtomicBool, Ordering};
63use std::time::Instant;
64
65type StagedFormulaEntry = (u32, u32, String);
66type StagedSheetParts = (
67    Vec<StagedFormulaEntry>,
68    Option<crate::engine::DeferredFormulaPackage>,
69);
70
71/// Per-sheet staged-formula store (NOTE(#126) follow-up).
72///
73/// Ingest consumers (`build_graph_all`/`build_graph_for_sheets`) walk staged
74/// entries in INSERTION order, so the order-preserving `Vec` stays the
75/// canonical storage; a `(row, col) → index` map removes the linear dup-scan
76/// that made `stage_formula_text`/`get_staged_formula_text` O(staged-on-sheet)
77/// per call (O(n²) for an n-formula deferred load on one sheet — ~570 ms for
78/// 50k stages, release, before the index). `stage`/`get` are O(1);
79/// `remove` keeps the old O(n) `Vec::remove` (rare path, order preserved).
80#[derive(Default)]
81pub(crate) struct StagedSheet {
82    entries: Vec<StagedFormulaEntry>,
83    index: FxHashMap<(u32, u32), usize>,
84    deferred_package: Option<crate::engine::DeferredFormulaPackage>,
85}
86
87impl StagedSheet {
88    fn invalidate_all_deferred_families(package: &mut crate::engine::DeferredFormulaPackage) {
89        package
90            .invalidated
91            .extend(package.families.iter().map(|family| family.source_id));
92        package.invalidated.extend(
93            package
94                .partitioned_families
95                .iter()
96                .map(|family| family.source_id),
97        );
98    }
99
100    fn invalidate_deferred_at(&mut self, row: u32, col: u32) {
101        let Some(package) = self.deferred_package.as_mut() else {
102            return;
103        };
104        if package.suppressed.contains(&(row, col)) {
105            // Previously consumed coordinates are already excluded from residual
106            // authority. Edits/deletion must not invalidate untouched fragments.
107            return;
108        }
109        if package.source_geometry_complete
110            && package.families.is_empty()
111            && package.partitioned_families.is_empty()
112        {
113            if let Some((row0, col0)) = row.checked_sub(1).zip(col.checked_sub(1))
114                && package
115                    .source_coordinates
116                    .binary_search(&crate::engine::SourceCoord {
117                        row: row0,
118                        col: col0,
119                    })
120                    .is_ok()
121            {
122                package.suppressed.insert((row, col));
123            }
124            return;
125        }
126        let family = package
127            .replay
128            .lock()
129            .ok()
130            .and_then(|mut replay| replay.formula_at(row, col).ok())
131            .flatten()
132            .and_then(|record| record.partition_owner.or(record.family));
133        if let Some(family) = family {
134            package.invalidated.insert(family);
135        } else {
136            // A poisoned lock, replay failure, malformed spool, or missing
137            // lookup result must never let a possibly edited family commit.
138            // Fail closed at package scope before suppressing the edited cell.
139            Self::invalidate_all_deferred_families(package);
140        }
141        package.suppressed.insert((row, col));
142    }
143
144    fn reconcile_attached_deferred_package(&mut self) {
145        let Some(package) = self.deferred_package.as_mut() else {
146            return;
147        };
148        if self.entries.is_empty() {
149            return;
150        }
151
152        let replayed = (|| {
153            let mut disposition = crate::engine::FormulaReplayDisposition::default();
154            for partition in &package.partitioned_families {
155                disposition.register_partition(partition, false)?;
156            }
157            package
158                .replay
159                .lock()
160                .map_err(|_| "deferred formula spool lock poisoned".to_string())?
161                .replay_partitioned(&disposition, &package.partitioned_families)
162        })();
163
164        let Ok(records) = replayed else {
165            Self::invalidate_all_deferred_families(package);
166            package
167                .suppressed
168                .extend(self.entries.iter().map(|(row, col, _)| (*row, *col)));
169            return;
170        };
171        let mut owners = FxHashMap::default();
172        for record in &records {
173            owners
174                .entry((record.row, record.col))
175                .or_insert(record.partition_owner.or(record.family));
176        }
177        for (row, col, _) in &self.entries {
178            if let Some(family) = owners.get(&(*row, *col)).copied().flatten() {
179                package.invalidated.insert(family);
180            } else {
181                Self::invalidate_all_deferred_families(package);
182            }
183            package.suppressed.insert((*row, *col));
184        }
185        package.reconciliation_replay = Some(records);
186    }
187
188    fn stage(&mut self, row: u32, col: u32, text: String) {
189        self.invalidate_deferred_at(row, col);
190        match self.index.entry((row, col)) {
191            std::collections::hash_map::Entry::Occupied(slot) => {
192                self.entries[*slot.get()].2 = text;
193            }
194            std::collections::hash_map::Entry::Vacant(slot) => {
195                slot.insert(self.entries.len());
196                self.entries.push((row, col, text));
197            }
198        }
199    }
200
201    fn remove(&mut self, row: u32, col: u32) -> Option<String> {
202        let deferred_text = self.get(row, col);
203        self.invalidate_deferred_at(row, col);
204        let Some(idx) = self.index.remove(&(row, col)) else {
205            return deferred_text;
206        };
207        let (_, _, text) = self.entries.remove(idx);
208        // `Vec::remove` shifted everything after `idx` left by one.
209        for slot in self.index.values_mut() {
210            if *slot > idx {
211                *slot -= 1;
212            }
213        }
214        Some(text)
215    }
216
217    fn get_ordinary(&self, row: u32, col: u32) -> Option<&str> {
218        self.index
219            .get(&(row, col))
220            .map(|&i| self.entries[i].2.as_str())
221    }
222
223    fn remove_ordinary(&mut self, row: u32, col: u32) -> Option<String> {
224        let idx = self.index.remove(&(row, col))?;
225        let (_, _, text) = self.entries.remove(idx);
226        for slot in self.index.values_mut() {
227            if *slot > idx {
228                *slot -= 1;
229            }
230        }
231        Some(text)
232    }
233
234    fn get(&self, row: u32, col: u32) -> Option<String> {
235        if let Some(text) = self.get_ordinary(row, col) {
236            return Some(text.to_string());
237        }
238        let package = self.deferred_package.as_ref()?;
239        if package.suppressed.contains(&(row, col)) {
240            return None;
241        }
242        package
243            .replay
244            .lock()
245            .ok()?
246            .formula_at(row, col)
247            .ok()?
248            .map(|record| record.text)
249    }
250
251    fn len(&self) -> usize {
252        self.deferred_package
253            .as_ref()
254            .map_or(self.entries.len(), |package| {
255                (if package.source_geometry_complete
256                    && (package.coordinates_cover_families
257                        || (package.families.is_empty() && package.partitioned_families.is_empty()))
258                {
259                    package.source_coordinates.len()
260                } else {
261                    usize::try_from(package.report.source_formula_records_spooled)
262                        .unwrap_or(usize::MAX)
263                })
264                .saturating_sub(package.suppressed.len())
265                .saturating_add(self.entries.len())
266            })
267    }
268
269    fn is_empty(&self) -> bool {
270        self.entries.is_empty() && self.deferred_package.is_none()
271    }
272
273    fn into_parts(self) -> StagedSheetParts {
274        (self.entries, self.deferred_package)
275    }
276}
277
278type StagedFormulaMap = std::collections::HashMap<String, StagedSheet>;
279
280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
281struct PreparationRegion {
282    sheet: String,
283    sheet_id: SheetId,
284    start_row: u32,
285    start_col: u32,
286    end_row: u32,
287    end_col: u32,
288}
289
290// Target roots dedupe by identity; they keep their first-seen order.
291struct OrderedTargetProducers {
292    ordered: Vec<crate::engine::target_preparation::TargetProducer>,
293    seen: FxHashSet<crate::engine::target_preparation::TargetProducer>,
294}
295
296impl OrderedTargetProducers {
297    fn with_capacity(capacity: usize) -> Result<Self, std::collections::TryReserveError> {
298        let mut ordered = Vec::new();
299        ordered.try_reserve(capacity)?;
300        let mut seen = FxHashSet::default();
301        seen.try_reserve(capacity)?;
302        Ok(Self { ordered, seen })
303    }
304
305    fn from_ordered(
306        ordered: Vec<crate::engine::target_preparation::TargetProducer>,
307    ) -> Result<Self, std::collections::TryReserveError> {
308        let mut seen = FxHashSet::default();
309        seen.try_reserve(ordered.len())?;
310        seen.extend(ordered.iter().copied());
311        Ok(Self { ordered, seen })
312    }
313
314    fn push(
315        &mut self,
316        producer: crate::engine::target_preparation::TargetProducer,
317    ) -> Result<bool, std::collections::TryReserveError> {
318        #[cfg(test)]
319        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(probes.get().saturating_add(1)));
320        if self.seen.contains(&producer) {
321            return Ok(false);
322        }
323        self.seen.try_reserve(1)?;
324        self.ordered.try_reserve(1)?;
325        self.seen.insert(producer);
326        self.ordered.push(producer);
327        Ok(true)
328    }
329
330    fn len(&self) -> usize {
331        self.ordered.len()
332    }
333
334    fn into_vec(self) -> Vec<crate::engine::target_preparation::TargetProducer> {
335        self.ordered
336    }
337}
338
339#[cfg(test)]
340thread_local! {
341    static TARGET_ROOT_DEDUP_PROBES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
342}
343
344fn target_root_allocation_error(observed: usize, request_id: Option<u64>) -> ExcelError {
345    crate::engine::ResourceLedgerError::Exhausted(formualizer_common::ResourceExhaustionDetail {
346        reason: formualizer_common::ResourceExhaustionReason::ScratchMemory,
347        limit: u64::MAX,
348        observed: observed as u64,
349        request_id,
350    })
351    .into_excel_error()
352}
353
354#[derive(Debug)]
355struct PreparedOrdinaryStagedFormula {
356    sheet: String,
357    sheet_id: SheetId,
358    lease: StagedFormulaLease,
359    ast_id: Option<AstNodeId>,
360    plan: Option<DependencyPlanRow>,
361}
362
363struct PreparedTargetSourcePackage {
364    sheet: String,
365    sheet_id: SheetId,
366    lease: StagedPackageLease,
367    selected_points: Option<BTreeSet<(u32, u32)>>,
368    complete_selections: BTreeSet<crate::engine::SourceFamilyId>,
369    deferred_shared: bool,
370    direct_domains: Vec<(
371        crate::engine::SourceFamilyId,
372        crate::engine::PlacementDomainTransport,
373    )>,
374    source_report: crate::engine::FormulaCompressedSourceReport,
375    replay_records: Vec<crate::engine::DeferredReplayFormula>,
376    spool_replays: u64,
377    disposition: crate::engine::FormulaReplayDisposition,
378    legacy: Vec<(u32, u32, AstNodeId, DependencyPlanRow)>,
379    direct_families: usize,
380    direct_cells: u64,
381    direct_fragments: u64,
382    direct_complete_families: u64,
383    direct_complete_cells: u64,
384    direct_partition_families: u64,
385    direct_partition_cells: u64,
386    anchor_parses: u64,
387    anchor_asts: u64,
388    anchor_analyses: u64,
389}
390
391impl PreparedTargetSourcePackage {
392    fn empty_selection(sheet: &str, sheet_id: SheetId, lease: StagedPackageLease) -> Self {
393        Self {
394            sheet: sheet.to_owned(),
395            sheet_id,
396            lease,
397            selected_points: Some(BTreeSet::new()),
398            complete_selections: Default::default(),
399            deferred_shared: false,
400            direct_domains: Vec::new(),
401            source_report: Default::default(),
402            replay_records: Vec::new(),
403            spool_replays: 0,
404            disposition: Default::default(),
405            legacy: Vec::new(),
406            direct_families: 0,
407            direct_cells: 0,
408            direct_fragments: 0,
409            direct_complete_families: 0,
410            direct_complete_cells: 0,
411            direct_partition_families: 0,
412            direct_partition_cells: 0,
413            anchor_parses: 0,
414            anchor_asts: 0,
415            anchor_analyses: 0,
416        }
417    }
418
419    fn direct_contains(&self, row: u32, col: u32) -> bool {
420        self.direct_domains.iter().any(|(_, domain)| {
421            let rect = domain.rect();
422            row > rect.start.row
423                && row <= rect.end.row + 1
424                && col > rect.start.col
425                && col <= rect.end.col + 1
426        })
427    }
428
429    fn fallback_records(&self) -> impl Iterator<Item = &crate::engine::DeferredReplayFormula> {
430        self.replay_records.iter().filter(|record| {
431            let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1)) else {
432                return true;
433            };
434            let coord = crate::engine::SourceCoord { row, col };
435            let disposition = match record.family {
436                Some(family) => self.disposition.shared_disposition(family, coord),
437                None => self.disposition.ordinary_disposition(coord).0,
438            };
439            !matches!(
440                disposition,
441                crate::engine::FormulaReplayCoordinateDisposition::Direct
442                    | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
443            )
444        })
445    }
446}
447
448type PreparedFormulaBatches = Vec<FormulaIngestBatch>;
449type StagedFormulaBatches = Vec<(String, StagedSheet)>;
450
451type CompressedReplayBatch = (
452    FormulaIngestBatch,
453    crate::engine::FormulaCompressedSourceBatch,
454);
455type PreparedSourceBatch = (
456    FormulaIngestBatch,
457    crate::engine::FormulaCompressedSourceReport,
458    crate::engine::FormulaCompressedPreparation,
459);
460type PreparedStagedFormulaBatches = (
461    PreparedFormulaBatches,
462    Vec<CompressedReplayBatch>,
463    Vec<PreparedSourceBatch>,
464    // Formulas whose planning can fail (see `formula_may_fail_planning`).
465    FxHashSet<crate::engine::arena::AstNodeId>,
466);
467
468/// Backend-neutral source-family ingress. Adapters may prepare candidates and
469/// submit exact replay; every formula is still materialized per cell.
470#[doc(hidden)]
471pub struct SourceFormulaIngress<'a, R> {
472    engine: &'a mut Engine<R>,
473}
474
475impl<R> SourceFormulaIngress<'_, R>
476where
477    R: EvaluationContext,
478{
479    pub fn prepare_families(
480        &mut self,
481        sheet_name: &str,
482        families: &[crate::engine::SourceFormulaFamily],
483    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
484        self.engine.observe_function_semantic_epoch()?;
485        Ok(self
486            .engine
487            .prepare_source_formula_families(sheet_name, families))
488    }
489
490    pub fn prepare_eager_proposals(
491        &mut self,
492        sheet_name: &str,
493        families: &[crate::engine::SourceFormulaFamily],
494        partitions: &[crate::engine::PartitionedSourceFormulaFamily],
495        formula_record_count: u64,
496        replay: Box<dyn crate::engine::DeferredFormulaReplay>,
497    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
498        self.engine.observe_function_semantic_epoch()?;
499        let replay = Arc::new(std::sync::Mutex::new(replay));
500        self.engine.prepare_source_formula_proposals(
501            sheet_name,
502            families,
503            partitions,
504            partitions,
505            formula_record_count,
506            replay,
507            &BTreeSet::new(),
508            &Default::default(),
509            None,
510        )
511    }
512
513    pub fn stage_deferred(&mut self, package: crate::engine::DeferredFormulaPackage) {
514        self.engine.stage_deferred_formula_package(package);
515    }
516
517    pub fn ingest_replay_batches(
518        &mut self,
519        batches: Vec<(
520            FormulaIngestBatch,
521            crate::engine::FormulaCompressedSourceBatch,
522        )>,
523    ) -> Result<FormulaIngestReport, ExcelError> {
524        self.engine
525            .ingest_compressed_formula_source_batches(batches)
526    }
527
528    pub fn finish_prepared(
529        &mut self,
530        batches: Vec<(
531            FormulaIngestBatch,
532            crate::engine::FormulaCompressedSourceReport,
533            crate::engine::FormulaCompressedPreparation,
534        )>,
535    ) -> Result<FormulaIngestReport, ExcelError> {
536        self.engine.finish_compressed_formula_sources(batches)
537    }
538}
539
540// Computed-write coalescing pays a fixed grouping/planning cost. For very narrow
541// layers there is not enough work to amortize it, and the direct point-write path
542// is faster while preserving the same visibility semantics.
543const COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH: usize = 8;
544
545/// Equal values, numbers by bits (debug oracles).
546#[cfg(debug_assertions)]
547fn same_value_bits(a: &LiteralValue, b: &LiteralValue) -> bool {
548    match (a, b) {
549        (LiteralValue::Number(x), LiteralValue::Number(y)) => x.to_bits() == y.to_bits(),
550        _ => a == b,
551    }
552}
553
554/// Whether a layer's computed writes are buffered: not for a chain unit,
555/// whose members read the ones written before them.
556fn buffer_layer_writes(layer: &crate::engine::scheduler::Layer) -> bool {
557    !layer.sequential && layer.vertices.len() >= COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH
558}
559/// Adaptive layer parallelism (see `evaluate_layer_parallel`): a layer runs
560/// sequentially until either it has run `PARALLEL_LAYER_PROBE` or the rest,
561/// at the rate so far, is estimated at `PARALLEL_LAYER_WORTH` or more; then
562/// the rest goes to the thread pool. Tuned on Enron first eval (probe 30 /
563/// 100 / 300 µs, worth 50 / 150 / 400 µs; 300 / 150 best, no workbook
564/// slower).
565/// Candidate count from which schedule preparation sorts on the pool.
566const PARALLEL_SCHEDULE_MIN_CANDIDATES: usize = 16 * 1024;
567/// Plan reuse: a schedule of more candidates than this can become the
568/// base, and a request takes the base restricted to it when the base is at
569/// most `BASE_SCHEDULE_RATIO` times the request.
570const BASE_SCHEDULE_MIN_VERTICES: usize = 64;
571const BASE_SCHEDULE_RATIO: usize = 256;
572/// A restricted schedule keeps the base's layers, which can split a family
573/// run the planner would keep whole (each piece then builds its own
574/// criteria index): below this many candidates planning costs less than
575/// that (real_ops_model: 75 candidates, 31 us to plan, +0.3 ms restricted).
576const BASE_SCHEDULE_MIN_REQUEST: usize = 128;
577const PARALLEL_LAYER_PROBE: std::time::Duration = std::time::Duration::from_micros(300);
578const PARALLEL_LAYER_WORTH: std::time::Duration = std::time::Duration::from_micros(150);
579/// A member this expensive (ns, measured by the probe) is its own task.
580const EXPENSIVE_VERTEX_NS: u128 = 10_000;
581/// [`Engine::live_cancellation_after_work`] messages.
582const UNIT_CANCELLED: &str = "Evaluation cancelled; the evaluated unit was not committed";
583const GROUP_CANCELLED: &str =
584    "Parallel evaluation cancelled; the evaluated group was not committed";
585const SCC_CANCELLED: &str = "Evaluation cancelled; the evaluated cycle was not completed";
586
587#[derive(Debug, Clone, PartialEq)]
588pub(crate) enum ComputedWrite {
589    Cell {
590        seq: u64,
591        sheet_id: SheetId,
592        row0: u32,
593        col0: u32,
594        value: OverlayValue,
595        format_id: Option<crate::format::FormatId>,
596    },
597    Rect {
598        seq: u64,
599        sheet_id: SheetId,
600        sr0: u32,
601        sc0: u32,
602        values: Vec<Vec<OverlayValue>>,
603    },
604    /// Consecutive rows `row0..` of one column (a family run's commit),
605    /// each with its format.
606    Run {
607        seq: u64,
608        sheet_id: SheetId,
609        row0: u32,
610        col0: u32,
611        entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
612    },
613}
614
615impl ComputedWrite {
616    #[inline]
617    pub(crate) fn seq(&self) -> u64 {
618        match self {
619            ComputedWrite::Cell { seq, .. }
620            | ComputedWrite::Rect { seq, .. }
621            | ComputedWrite::Run { seq, .. } => *seq,
622        }
623    }
624}
625
626#[derive(Debug, Default)]
627pub(crate) struct ComputedWriteBuffer {
628    writes: Vec<ComputedWrite>,
629    next_seq: u64,
630    estimated_bytes: usize,
631    formats_present: bool,
632}
633
634impl ComputedWriteBuffer {
635    const ENTRY_BASE_BYTES: usize = 32;
636
637    #[inline]
638    pub(crate) fn is_empty(&self) -> bool {
639        self.writes.is_empty()
640    }
641
642    #[inline]
643    pub(crate) fn len(&self) -> usize {
644        self.writes.len()
645    }
646
647    #[inline]
648    pub(crate) fn writes(&self) -> &[ComputedWrite] {
649        &self.writes
650    }
651
652    #[inline]
653    pub(crate) fn estimated_bytes(&self) -> usize {
654        self.estimated_bytes
655    }
656
657    #[cfg(feature = "tracing")]
658    fn traced_cell_count(&self) -> usize {
659        self.writes
660            .iter()
661            .map(|write| match write {
662                ComputedWrite::Cell { .. } => 1,
663                ComputedWrite::Rect { values, .. } => values.iter().map(Vec::len).sum(),
664                ComputedWrite::Run { entries, .. } => entries.len(),
665            })
666            .sum()
667    }
668
669    pub(crate) fn push_cell(
670        &mut self,
671        sheet_id: SheetId,
672        row0: u32,
673        col0: u32,
674        value: OverlayValue,
675    ) {
676        self.push_cell_with_format(sheet_id, row0, col0, value, None);
677    }
678
679    pub(crate) fn push_cell_with_format(
680        &mut self,
681        sheet_id: SheetId,
682        row0: u32,
683        col0: u32,
684        value: OverlayValue,
685        format_id: Option<crate::format::FormatId>,
686    ) {
687        let format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
688        self.formats_present |= format_id.is_some();
689        let seq = self.next_sequence();
690        self.estimated_bytes = self
691            .estimated_bytes
692            .saturating_add(Self::estimate_value_bytes(&value));
693        self.writes.push(ComputedWrite::Cell {
694            seq,
695            sheet_id,
696            row0,
697            col0,
698            value,
699            format_id,
700        });
701    }
702
703    pub(crate) fn push_column_run(
704        &mut self,
705        sheet_id: SheetId,
706        row0: u32,
707        col0: u32,
708        mut entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
709    ) {
710        let seq = self.next_sequence();
711        let mut added = 0usize;
712        for (value, format_id) in entries.iter_mut() {
713            *format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
714            self.formats_present |= format_id.is_some();
715            added = added.saturating_add(Self::estimate_value_bytes(value));
716        }
717        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
718        self.writes.push(ComputedWrite::Run {
719            seq,
720            sheet_id,
721            row0,
722            col0,
723            entries,
724        });
725    }
726
727    pub(crate) fn push_rect(
728        &mut self,
729        sheet_id: SheetId,
730        sr0: u32,
731        sc0: u32,
732        values: Vec<Vec<OverlayValue>>,
733    ) {
734        let seq = self.next_sequence();
735        let added = values
736            .iter()
737            .flat_map(|row| row.iter())
738            .map(Self::estimate_value_bytes)
739            .fold(0usize, usize::saturating_add);
740        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
741        self.writes.push(ComputedWrite::Rect {
742            seq,
743            sheet_id,
744            sr0,
745            sc0,
746            values,
747        });
748    }
749
750    pub(crate) fn clear(&mut self) {
751        self.writes.clear();
752        self.estimated_bytes = 0;
753        self.formats_present = false;
754    }
755
756    fn take_writes(&mut self) -> (Vec<ComputedWrite>, bool) {
757        self.estimated_bytes = 0;
758        let formats_present = std::mem::take(&mut self.formats_present);
759        (std::mem::take(&mut self.writes), formats_present)
760    }
761
762    fn next_sequence(&mut self) -> u64 {
763        let seq = self.next_seq;
764        self.next_seq = self.next_seq.wrapping_add(1);
765        seq
766    }
767
768    #[inline]
769    fn estimate_value_bytes(value: &OverlayValue) -> usize {
770        Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
771    }
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
775struct ComputedWriteChunkKey {
776    sheet_id: SheetId,
777    col0: u32,
778    chunk_idx: usize,
779    chunk_start_row0: u32,
780}
781
782#[derive(Debug, Clone, PartialEq)]
783pub(crate) struct ComputedWriteChunkEntryPlan {
784    pub(crate) row_in_chunk: usize,
785    pub(crate) seq: u64,
786    pub(crate) value: OverlayValue,
787    pub(crate) format_id: Option<crate::format::FormatId>,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq)]
791pub(crate) enum ComputedWriteChunkPlanShape {
792    Point,
793    SparseOffsets {
794        entries: usize,
795        span_len: usize,
796    },
797    DenseRange {
798        start: usize,
799        len: usize,
800    },
801    RunRange {
802        start: usize,
803        len: usize,
804        runs: usize,
805    },
806}
807
808#[derive(Debug, Clone, PartialEq, Eq)]
809pub(crate) enum ComputedWriteFormatClear {
810    Range { start: usize, end: usize },
811    Offsets(Vec<usize>),
812}
813
814#[derive(Debug, Clone, PartialEq)]
815pub(crate) enum ComputedWriteChunkFormatEffect {
816    NoFormatWork,
817    ClearStale(ComputedWriteFormatClear),
818    SetExplicit(Vec<(usize, Option<crate::format::FormatId>)>),
819}
820
821#[derive(Debug, Clone, PartialEq)]
822pub(crate) struct ComputedWriteChunkPlan {
823    pub(crate) sheet_id: SheetId,
824    pub(crate) col0: u32,
825    pub(crate) chunk_idx: usize,
826    pub(crate) chunk_start_row0: u32,
827    pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
828    pub(crate) shape: ComputedWriteChunkPlanShape,
829    pub(crate) format_effect: ComputedWriteChunkFormatEffect,
830}
831
832#[derive(Debug, Clone, Default, PartialEq)]
833pub(crate) struct ComputedWriteCoalescingPlan {
834    pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
835    pub(crate) input_cells: usize,
836    pub(crate) coalesced_cells: usize,
837    pub(crate) overwritten_cells: usize,
838}
839
840impl ComputedWriteCoalescingPlan {
841    #[inline]
842    pub(crate) fn is_empty(&self) -> bool {
843        self.chunks.is_empty()
844    }
845}
846
847impl ComputedWriteChunkPlan {
848    fn from_group(
849        key: ComputedWriteChunkKey,
850        mut entries: Vec<ComputedWriteChunkEntryPlan>,
851        formats_present: bool,
852        computed_lane_has_formats: bool,
853    ) -> (Self, usize) {
854        entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
855        let input_len = entries.len();
856        let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
857        for entry in entries {
858            if let Some(prev) = coalesced.last_mut()
859                && prev.row_in_chunk == entry.row_in_chunk
860            {
861                *prev = entry;
862                continue;
863            }
864            coalesced.push(entry);
865        }
866        let overwritten = input_len.saturating_sub(coalesced.len());
867        let shape = Self::classify_shape(&coalesced);
868        let format_effect = Self::classify_format_effect(
869            &coalesced,
870            &shape,
871            formats_present,
872            computed_lane_has_formats,
873        );
874        (
875            Self {
876                sheet_id: key.sheet_id,
877                col0: key.col0,
878                chunk_idx: key.chunk_idx,
879                chunk_start_row0: key.chunk_start_row0,
880                entries: coalesced,
881                shape,
882                format_effect,
883            },
884            overwritten,
885        )
886    }
887
888    fn classify_format_effect(
889        entries: &[ComputedWriteChunkEntryPlan],
890        shape: &ComputedWriteChunkPlanShape,
891        formats_present: bool,
892        computed_lane_has_formats: bool,
893    ) -> ComputedWriteChunkFormatEffect {
894        if !formats_present {
895            return if computed_lane_has_formats {
896                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
897            } else {
898                ComputedWriteChunkFormatEffect::NoFormatWork
899            };
900        }
901
902        if entries.iter().all(|entry| entry.format_id.is_none()) {
903            return if computed_lane_has_formats {
904                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
905            } else {
906                ComputedWriteChunkFormatEffect::NoFormatWork
907            };
908        }
909
910        ComputedWriteChunkFormatEffect::SetExplicit(
911            entries
912                .iter()
913                .map(|entry| (entry.row_in_chunk, entry.format_id))
914                .collect(),
915        )
916    }
917
918    fn format_clear_spec(
919        entries: &[ComputedWriteChunkEntryPlan],
920        shape: &ComputedWriteChunkPlanShape,
921    ) -> ComputedWriteFormatClear {
922        match shape {
923            ComputedWriteChunkPlanShape::DenseRange { start, len }
924            | ComputedWriteChunkPlanShape::RunRange { start, len, .. } => {
925                ComputedWriteFormatClear::Range {
926                    start: *start,
927                    end: start.saturating_add(*len),
928                }
929            }
930            ComputedWriteChunkPlanShape::Point
931            | ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
932                ComputedWriteFormatClear::Offsets(
933                    entries.iter().map(|entry| entry.row_in_chunk).collect(),
934                )
935            }
936        }
937    }
938
939    fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
940        debug_assert!(!entries.is_empty());
941        if entries.len() == 1 {
942            return ComputedWriteChunkPlanShape::Point;
943        }
944
945        let start = entries[0].row_in_chunk;
946        let end = entries[entries.len() - 1].row_in_chunk;
947        let span_len = end.saturating_sub(start).saturating_add(1);
948        if span_len != entries.len() {
949            return ComputedWriteChunkPlanShape::SparseOffsets {
950                entries: entries.len(),
951                span_len,
952            };
953        }
954
955        let runs = Self::run_count(entries);
956        if runs < entries.len() {
957            ComputedWriteChunkPlanShape::RunRange {
958                start,
959                len: entries.len(),
960                runs,
961            }
962        } else {
963            ComputedWriteChunkPlanShape::DenseRange {
964                start,
965                len: entries.len(),
966            }
967        }
968    }
969
970    fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
971        let mut runs = 0usize;
972        let mut prev: Option<&OverlayValue> = None;
973        for entry in entries {
974            if prev != Some(&entry.value) {
975                runs = runs.saturating_add(1);
976                prev = Some(&entry.value);
977            }
978        }
979        runs
980    }
981}
982
983#[cfg(feature = "tracing")]
984#[derive(Default)]
985struct TraceEvaluationCounters {
986    computed_vertices: usize,
987    cycles: usize,
988}
989
990pub struct Engine<R> {
991    pub(crate) graph: DependencyGraph,
992    resolver: R,
993    pub config: EvalConfig,
994    workbook_load_limits: crate::engine::WorkbookLoadLimits,
995    /// Clock for volatile date/time builtins, wrapped in a per-recalc
996    /// snapshot: sampled once at the start of every evaluation request
997    /// ([`Self::begin_evaluation_request`]) so all `NOW()`/`TODAY()` reads in
998    /// one recalc — including SCC iteration passes — agree (spec §7.11).
999    clock: crate::timezone::SnapshotClock,
1000    thread_pool: Option<Arc<rayon::ThreadPool>>,
1001    pub recalc_epoch: u64,
1002    snapshot_id: std::sync::atomic::AtomicU64,
1003    topology_epoch: u64,
1004    cached_static_schedule: Option<CachedScheduleEntry>,
1005    /// Program 3 (plan reuse): schedules of recent earlier requests, most
1006    /// recent last (a user alternating between a few inputs recalculates
1007    /// the same few closures). Bounded by `RECENT_SCHEDULES` entries and
1008    /// `RECENT_SCHEDULE_VERTICES` candidate vertices in total.
1009    recent_schedules: Vec<CachedScheduleEntry>,
1010    /// Program 3 (plan reuse): the largest current schedule seen (usually
1011    /// the first evaluation's). A request it covers, and that is not much
1012    /// smaller, takes its schedule restricted to the request instead of
1013    /// planning (`Schedule::restrict`).
1014    base_schedule: Option<CachedScheduleEntry>,
1015    #[cfg(any(test, feature = "benchmark_internal"))]
1016    recalc_reuse_probe: std::sync::Mutex<RecalcReuseProbe>,
1017    spill_mgr: ShimSpillManager,
1018    /// Arrow-backed storage for sheet values (Phase A)
1019    arrow_sheets: SheetStore,
1020    /// Workbook-local number-format registry.
1021    format_registry: crate::format::FormatRegistry,
1022    /// Derived formula formats keyed by grid position, never graph vertex identity.
1023    derived_formats: crate::engine::derived_formats::DerivedFormats,
1024    #[cfg(test)]
1025    derived_format_operations_for_test: std::sync::atomic::AtomicU64,
1026    #[cfg(test)]
1027    family_members_for_test: std::sync::atomic::AtomicU64,
1028    #[cfg(test)]
1029    invariant_bound_members_for_test: std::sync::atomic::AtomicU64,
1030    #[cfg(test)]
1031    lifted_members_for_test: std::sync::atomic::AtomicU64,
1032    #[cfg(test)]
1033    chained_members_for_test: std::sync::atomic::AtomicU64,
1034    #[cfg(test)]
1035    lane_clean_reads_for_test: std::sync::atomic::AtomicU64,
1036    #[cfg(test)]
1037    criteria_kernel_members_for_test: std::sync::atomic::AtomicU64,
1038    #[cfg(test)]
1039    memo_hits_for_test: std::sync::atomic::AtomicU64,
1040    /// Authority build last compressed (`maybe_compress_formulas`).
1041    compressed_at_build: Option<u64>,
1042    #[cfg(test)]
1043    computed_overlay_set_explicit_entry_operations_for_test: u64,
1044    #[cfg(test)]
1045    computed_overlay_stale_clear_range_effects_for_test: u64,
1046    #[cfg(test)]
1047    computed_overlay_stale_clear_offset_attempts_for_test: u64,
1048    #[cfg(test)]
1049    computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64,
1050    /// True if any edit after bulk load; disables Arrow reads for parity
1051    has_edited: bool,
1052    /// Overlay compaction counter (Phase C instrumentation)
1053    overlay_compactions: u64,
1054
1055    // Overlay memory observability / budget (ticket 503)
1056    computed_overlay_bytes_estimate: usize,
1057    computed_overlay_mirroring_disabled: bool,
1058    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
1059    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
1060    pub(crate) force_materialize_range_views: bool,
1061    // Pass-scoped cache for Arrow used-row bounds per column
1062    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
1063    // Snapshot-scoped final used-axis bounds for open-ended references.
1064    used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
1065    lookup_index_cache: LookupIndexCache,
1066    source_cache: Arc<std::sync::RwLock<SourceCache>>,
1067    /// Identity binding for opaque source-family preparations.
1068    source_formula_token: Arc<()>,
1069    /// Dedicated identity binding for reusable recalculation plans.
1070    recalc_plan_token: Arc<()>,
1071    /// Staged formulas by sheet when `defer_graph_building` is enabled.
1072    staged_formulas: StagedFormulaMap,
1073    /// Presence and generation authority for ordinary staged formula discovery.
1074    staged_formula_index: StagedFormulaIndex,
1075    // Occupancy invalidation only: never a formula/read dependency.
1076    blocked_pending_spills: Vec<(VertexId, CellRef, Region)>,
1077    /// Per-sheet row visibility sidecar state.
1078    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
1079    /// Cached row visibility masks keyed by sheet/span/mode/version.
1080    row_visibility_mask_cache: std::sync::RwLock<
1081        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
1082    >,
1083    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
1084    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
1085    /// Last centralized formula ingest report.
1086    last_formula_ingest_report: Option<FormulaIngestReport>,
1087    /// Aggregate centralized formula ingest report for this engine.
1088    formula_ingest_report_total: FormulaIngestReport,
1089    /// Transient cancellation flag used during evaluation
1090    active_cancel_flag: Option<crate::engine::CancelToken>,
1091    /// Transient absolute deadline used by composed target and plan requests.
1092    active_evaluation_deadline: Option<Instant>,
1093
1094    /// Engine-level action depth.
1095    ///
1096    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
1097    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
1098    action_depth: u32,
1099
1100    // Phase 3b virtual-dependency convergence telemetry
1101    last_virtual_dep_telemetry: VirtualDepTelemetry,
1102    virtual_dep_fallback_activations: u64,
1103
1104    // Runtime-cycle SCC evaluation telemetry (RFC #112, Stage 2)
1105    last_cycle_telemetry: CycleTelemetry,
1106
1107    // C0 evaluation-resource observability. IDs are never reset or reused.
1108    next_evaluation_resource_request_id: u64,
1109    evaluation_resource_request_depth: usize,
1110    active_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1111    last_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1112    evaluation_resource_baseline: EvaluationResourceBaselineStats,
1113    evaluation_resource_request_started_at: Option<crate::instant::FzInstant>,
1114    evaluation_resource_budgets: crate::engine::EvaluationBudgets,
1115    evaluation_resource_config_diagnostic:
1116        Option<crate::engine::EvaluationResourceConfigDiagnostic>,
1117    active_resource_ledger: Option<ResourceLedger>,
1118    source_cache_footprints: Vec<std::sync::Weak<std::sync::atomic::AtomicU64>>,
1119    source_cache_accounted: u64,
1120
1121    /// SCC members that entered iterative calculation (`CyclePolicy::Iterate`
1122    /// with a witnessed live cycle) during the current evaluation request
1123    /// and must re-run on the next one.
1124    ///
1125    /// Excel re-evaluates circular cells on EVERY recalc (the accumulator
1126    /// contract, spec §4/§7.6), but this engine's dirty model marks SCC
1127    /// members clean after a recalc and would otherwise skip them forever.
1128    /// Resolution: members of iterating SCCs are redirtied volatile-like at
1129    /// the end of the same recalc that iterated them
1130    /// ([`Self::redirty_for_next_recalc`], called wherever
1131    /// `redirty_volatiles` runs). The set is per-recalc, never persisted:
1132    /// if an edit breaks the cycle, the next recalc's SCC task either does
1133    /// not exist or settles as phantom, nothing re-registers, and the
1134    /// redirty chain stops by itself.
1135    ///
1136    /// SCCs that landed on an exact fixed point are exempt: they go to
1137    /// [`Self::retained_scc_members`] instead and stay clean until the dirty
1138    /// graph (or a config change) reaches them (#368).
1139    pending_iterative_redirty: Vec<VertexId>,
1140    /// Members of iterating SCCs retained across recalcs (#368), keyed to the
1141    /// id of the retained SCC they belong to (ids come from
1142    /// `next_retained_scc_id`; grouping is only used for telemetry).
1143    ///
1144    /// An SCC is retained when the recalc that iterated it stopped because
1145    /// every member reproduced its previous value exactly (|Δ| = 0, or
1146    /// identity for non-numeric members; never NaN-converged), before the
1147    /// `max_iterations` cap, with no volatile or dynamic-reference member.
1148    /// Such an SCC is a fixed point of its own inputs: running it again with
1149    /// the same inputs cannot change any value, so it is not redirtied. The
1150    /// dirty graph remains the validity authority — any edit that reaches a
1151    /// member dirties it like any other formula and the SCC task re-runs.
1152    /// Membership is dropped when a member runs in an SCC task again (it is
1153    /// then re-retained or re-registered for per-recalc redirty), when the
1154    /// vertex is deleted, or when
1155    /// [`Self::reconcile_retained_sccs_at_request_begin`] invalidates the
1156    /// whole set because a config knob outside the graph changed.
1157    retained_scc_members: FxHashMap<VertexId, u64>,
1158    next_retained_scc_id: u64,
1159    /// [`Self::retained_scc_config_fingerprint`] as of the last retention.
1160    /// `Engine::config` is a public field, so knobs that change a retained
1161    /// SCC's result (cycle policy/tolerance, date system, determinism,
1162    /// volatile seeding) can change between recalcs without touching the
1163    /// graph; a mismatch at request begin dirties every retained member.
1164    /// Only meaningful while `retained_scc_members` is non-empty.
1165    retained_scc_config_fingerprint: u64,
1166    /// Function-registry semantic epoch and runtime-provider revision as of
1167    /// the last time retained SCCs were reconciled against them. A newer
1168    /// epoch dirties only the retained members whose formula calls a changed
1169    /// function (or every member when the change log is incomplete).
1170    retained_scc_function_epoch_seen: u64,
1171    retained_scc_provider_revision_seen: Option<u64>,
1172    /// Retained members that were already dirty when the current request
1173    /// began, with the SCC id they carried. A member still holding that id
1174    /// at the end of the request was not touched by any SCC task — its
1175    /// cycle dissolved (it evaluated as an ordinary formula) or the request
1176    /// never reached it — so [`Self::redirty_for_next_recalc`] drops it from
1177    /// the retained set instead of letting it linger.
1178    retained_scc_dirty_at_begin: Vec<(VertexId, u64)>,
1179
1180    /// Final committed values of iterating-SCC members (spec §4 persistence).
1181    /// In canonical (value-cache disabled) mode the computed overlay is the
1182    /// ONLY home of a formula's value, and structural edits clear computed
1183    /// overlays wholesale (`clear_computed_overlay_after_row/_col`) —
1184    /// destroying iteration state (accumulators reset to 0; found by the
1185    /// iterate edge corpus). This snapshot lets the next SCC task re-seed
1186    /// members whose overlay entry vanished. Members registered for
1187    /// per-recalc redirty are refreshed by
1188    /// [`Self::redirty_for_next_recalc`]; retained members are written once
1189    /// when retained. Entries are dropped when the member's SCC task ends
1190    /// without iterating or when the vertex is deleted. Empty unless
1191    /// something iterated — zero cost otherwise.
1192    iterative_state_values: FxHashMap<VertexId, LiteralValue>,
1193
1194    /// Global function-registry semantic epoch last observed.
1195    function_semantic_epoch_seen: u64,
1196    /// Runtime-provider semantic revision last observed.
1197    function_provider_revision_seen: Option<u64>,
1198
1199    #[cfg(feature = "tracing")]
1200    trace_evaluation_counters: TraceEvaluationCounters,
1201    #[cfg(test)]
1202    evaluation_request_begin_count_for_test: u64,
1203    #[cfg(any(test, feature = "test-support"))]
1204    before_prepared_span_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1205    #[cfg(test)]
1206    before_target_preparation_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1207    #[cfg(test)]
1208    before_target_planning_snapshot_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1209    #[cfg(test)]
1210    inject_target_semantic_stale_once_for_test: bool,
1211    #[cfg(test)]
1212    force_virtual_dep_changes_remaining_for_test: usize,
1213    /// Dynamic-read freshness state (design §8.2).
1214    freshness: freshness::Freshness,
1215    #[cfg(test)]
1216    fail_evaluation_commit_preflight_once_for_test: bool,
1217    #[cfg(test)]
1218    target_preparation_fault_for_test:
1219        Option<crate::engine::target_preparation::TargetPreparationFault>,
1220    #[cfg(test)]
1221    force_non_cycle_schedule_fallback_for_test: bool,
1222    #[cfg(test)]
1223    before_legacy_fallback_final_provider_sample_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1224    #[cfg(test)]
1225    after_eager_proposal_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1226}
1227
1228/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
1229/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
1230impl<R: EvaluationContext> Engine<R> {
1231    pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
1232        self.graph.ingest_pipeline(&self.resolver)
1233    }
1234}
1235
1236pub struct EngineAction<'a, R>
1237where
1238    R: EvaluationContext,
1239{
1240    engine: &'a mut Engine<R>,
1241    name: String,
1242    // Complete private mutation capture used by atomic actions.
1243    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
1244    capture: Option<*mut MutationCapture>,
1245    // Optional Arrow undo journal used by `Engine::action_atomic`.
1246    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
1247    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
1248    // True when this EngineAction must enforce conservative atomic transaction policy.
1249    atomic_policy: bool,
1250}
1251
1252impl<'a, R> EngineAction<'a, R>
1253where
1254    R: EvaluationContext,
1255{
1256    #[inline]
1257    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
1258        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1259        let coord = crate::reference::Coord::from_excel(row, col, true, true);
1260        crate::reference::CellRef::new(sheet_id, coord)
1261    }
1262
1263    #[inline]
1264    pub fn name(&self) -> &str {
1265        &self.name
1266    }
1267
1268    #[inline]
1269    pub fn set_cell_value(
1270        &mut self,
1271        sheet: &str,
1272        row: u32,
1273        col: u32,
1274        value: LiteralValue,
1275    ) -> Result<(), crate::engine::EditorError> {
1276        if self.capture.is_some() {
1277            let old_value = self.engine.read_cell_value(sheet, row, col);
1278            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1279            let addr = self.addr_for(sheet, row, col);
1280            let Some(capture_ptr) = self.capture else {
1281                return Err(crate::engine::EditorError::TransactionFailed {
1282                    reason: "action_with_logger: missing mutation capture".to_string(),
1283                });
1284            };
1285
1286            // For atomic journal mode, record computed overlay effects for this cell.
1287            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
1288            let old_comp = if self.arrow_undo.is_some() {
1289                self.engine.read_computed_overlay_cell(sheet, row, col)
1290            } else {
1291                None
1292            };
1293
1294            if self.engine.graph_admission_enabled() {
1295                let admission =
1296                    self.engine
1297                        .graph
1298                        .preview_value_mutation(addr.sheet_id, row, col)?;
1299                self.engine.preflight_graph_admission(admission)?;
1300            }
1301            if old_formula.is_none() {
1302                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1303            }
1304
1305            let delta_old_sem = if old_formula.is_some() {
1306                None
1307            } else {
1308                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1309            };
1310
1311            let start_len = unsafe { (&*capture_ptr).len() };
1312
1313            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1314            let capture = unsafe { &mut *capture_ptr };
1315            self.engine.edit_with_capture(capture, |editor| {
1316                editor.set_cell_value_with_old_state(
1317                    addr,
1318                    value.clone(),
1319                    old_value.clone(),
1320                    old_formula.clone(),
1321                );
1322            })?;
1323            self.engine.record_structural_change(StructuralScope::Cell {
1324                sheet: addr.sheet_id,
1325                row: addr.coord.row(),
1326                col: addr.coord.col(),
1327            });
1328
1329            if let Some(undo_ptr) = self.arrow_undo {
1330                // 1) Spill snapshot operations (computed overlay rect restore).
1331                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1332                let undo = unsafe { &mut *undo_ptr };
1333                self.engine
1334                    .record_spill_ops_into_arrow_undo(undo, new_events);
1335
1336                // 2) Delta/computed overlay single-cell deltas.
1337                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
1338                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1339                let row0 = row.saturating_sub(1);
1340                let col0 = col.saturating_sub(1);
1341                let delta_new_sem = Some(value.clone());
1342                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
1343                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
1344            }
1345            Ok(())
1346        } else {
1347            self.engine
1348                .set_cell_value(sheet, row, col, value)
1349                .map_err(crate::engine::EditorError::from)
1350        }
1351    }
1352
1353    #[inline]
1354    pub fn set_cell_formula(
1355        &mut self,
1356        sheet: &str,
1357        row: u32,
1358        col: u32,
1359        ast: ASTNode,
1360    ) -> Result<(), crate::engine::EditorError> {
1361        if self.capture.is_some() {
1362            let old_value = self.engine.read_cell_value(sheet, row, col);
1363            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1364            let addr = self.addr_for(sheet, row, col);
1365            let Some(capture_ptr) = self.capture else {
1366                return Err(crate::engine::EditorError::TransactionFailed {
1367                    reason: "action_with_logger: missing mutation capture".to_string(),
1368                });
1369            };
1370
1371            let admitted_formula = if self.engine.graph_admission_enabled() {
1372                let placement =
1373                    CellRef::new(addr.sheet_id, Coord::from_excel(row, col, true, true));
1374                let ingested = self.engine.ingest_pipeline().ingest_formula(
1375                    FormulaAstInput::Tree(ast.clone()),
1376                    placement,
1377                    None,
1378                )?;
1379                let admission = self.engine.graph.preview_formula_mutations(&[(
1380                    addr.sheet_id,
1381                    row,
1382                    col,
1383                    ingested.dep_plan.clone(),
1384                )])?;
1385                self.engine.preflight_graph_admission(admission)?;
1386                Some((ingested.ast_id, ingested.dep_plan))
1387            } else {
1388                None
1389            };
1390            if old_formula.is_none() {
1391                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1392            }
1393            let delta_old = if self.arrow_undo.is_some() {
1394                if old_formula.is_some() {
1395                    None
1396                } else {
1397                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1398                }
1399            } else {
1400                None
1401            };
1402            let start_len = unsafe { (&*capture_ptr).len() };
1403
1404            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1405            let capture = unsafe { &mut *capture_ptr };
1406            self.engine.edit_with_capture(capture, |editor| {
1407                if let Some((ast_id, plan)) = admitted_formula {
1408                    editor.set_cell_formula_with_prepared_plan(
1409                        addr,
1410                        ast.clone(),
1411                        old_value,
1412                        old_formula,
1413                        ast_id,
1414                        plan,
1415                    );
1416                } else {
1417                    editor.set_cell_formula_with_old_state(
1418                        addr,
1419                        ast.clone(),
1420                        old_value,
1421                        old_formula,
1422                    );
1423                }
1424            })?;
1425            self.engine.record_structural_change(StructuralScope::Cell {
1426                sheet: addr.sheet_id,
1427                row: addr.coord.row(),
1428                col: addr.coord.col(),
1429            });
1430
1431            if let Some(undo_ptr) = self.arrow_undo {
1432                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1433                let undo = unsafe { &mut *undo_ptr };
1434                self.engine
1435                    .record_spill_ops_into_arrow_undo(undo, new_events);
1436                let delta_new: Option<LiteralValue> = None;
1437                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1438                let row0 = row.saturating_sub(1);
1439                let col0 = col.saturating_sub(1);
1440                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
1441            }
1442            Ok(())
1443        } else {
1444            self.engine
1445                .set_cell_formula(sheet, row, col, ast)
1446                .map_err(crate::engine::EditorError::from)
1447        }
1448    }
1449
1450    #[inline]
1451    pub fn set_row_hidden(
1452        &mut self,
1453        sheet: &str,
1454        row_1based: u32,
1455        hidden: bool,
1456        source: RowVisibilitySource,
1457    ) -> Result<(), crate::engine::EditorError> {
1458        if self.capture.is_some() {
1459            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1460            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
1461            let old_hidden = self
1462                .engine
1463                .row_visibility
1464                .get(&sheet_id)
1465                .map(|state| state.is_row_hidden(row0, Some(source)))
1466                .unwrap_or(false);
1467            if old_hidden == hidden {
1468                return Ok(());
1469            }
1470
1471            let _ = self
1472                .engine
1473                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1474
1475            let Some(capture_ptr) = self.capture else {
1476                return Err(crate::engine::EditorError::TransactionFailed {
1477                    reason: "action_with_logger: missing mutation capture".to_string(),
1478                });
1479            };
1480            unsafe { &mut *capture_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
1481                sheet_id,
1482                row0,
1483                source,
1484                old_hidden,
1485                new_hidden: hidden,
1486            });
1487
1488            Ok(())
1489        } else {
1490            self.engine
1491                .set_row_hidden(sheet, row_1based, hidden, source)
1492        }
1493    }
1494
1495    #[inline]
1496    pub fn set_rows_hidden(
1497        &mut self,
1498        sheet: &str,
1499        start_row_1based: u32,
1500        end_row_1based: u32,
1501        hidden: bool,
1502        source: RowVisibilitySource,
1503    ) -> Result<(), crate::engine::EditorError> {
1504        if self.capture.is_some() {
1505            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1506            let (start_row0, end_row0) =
1507                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
1508
1509            let Some(capture_ptr) = self.capture else {
1510                return Err(crate::engine::EditorError::TransactionFailed {
1511                    reason: "action_with_logger: missing mutation capture".to_string(),
1512                });
1513            };
1514            let capture = unsafe { &mut *capture_ptr };
1515
1516            for row0 in start_row0..=end_row0 {
1517                let old_hidden = self
1518                    .engine
1519                    .row_visibility
1520                    .get(&sheet_id)
1521                    .map(|state| state.is_row_hidden(row0, Some(source)))
1522                    .unwrap_or(false);
1523                if old_hidden == hidden {
1524                    continue;
1525                }
1526
1527                let _ = self
1528                    .engine
1529                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1530
1531                capture.record(crate::engine::ChangeEvent::SetRowVisibility {
1532                    sheet_id,
1533                    row0,
1534                    source,
1535                    old_hidden,
1536                    new_hidden: hidden,
1537                });
1538            }
1539
1540            Ok(())
1541        } else {
1542            self.engine
1543                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
1544        }
1545    }
1546
1547    #[inline]
1548    pub fn insert_rows(
1549        &mut self,
1550        sheet: &str,
1551        before: u32,
1552        count: u32,
1553    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1554        if count == 0 {
1555            return Ok(crate::engine::ShiftSummary::default());
1556        }
1557        if self.capture.is_some() {
1558            let Some(capture_ptr) = self.capture else {
1559                return Err(crate::engine::EditorError::TransactionFailed {
1560                    reason: "action_atomic: missing mutation capture".to_string(),
1561                });
1562            };
1563
1564            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1565            let before0 = before.saturating_sub(1);
1566            let occupancy = self.engine.structural_row_occupancy(sheet, sheet_id);
1567            let affected_region = Engine::<R>::structural_row_region(sheet_id, before0);
1568
1569            // Graph structural insert (logged) - no snapshot bump.
1570            let summary = {
1571                let capture = unsafe { &mut *capture_ptr };
1572                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1573                    Ok(crate::engine::ShiftSummary::default());
1574                self.engine.edit_with_capture(capture, |editor| {
1575                    editor.set_structural_occupancy(occupancy);
1576                    out = editor.insert_rows(sheet_id, before0, count);
1577                })?;
1578                out?
1579            };
1580
1581            // Arrow insert (truth) + undo op.
1582            self.engine.ensure_arrow_sheet(sheet);
1583            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1584                asheet.insert_rows(before0 as usize, count as usize);
1585            }
1586            self.engine
1587                .purge_derived_formats_after_row(sheet_id, before0);
1588            self.engine
1589                .shift_row_visibility_insert(sheet_id, before0, count);
1590            self.engine.mark_moved_formula_vertices_dirty(&summary);
1591            self.engine
1592                .clear_computed_overlay_after_row(sheet, before0 as usize);
1593            self.engine
1594                .record_structural_change(StructuralScope::Region(affected_region));
1595            if let Some(undo_ptr) = self.arrow_undo {
1596                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
1597            }
1598            Ok(summary)
1599        } else {
1600            self.engine.insert_rows(sheet, before, count)
1601        }
1602    }
1603
1604    #[inline]
1605    pub fn delete_rows(
1606        &mut self,
1607        sheet: &str,
1608        start: u32,
1609        count: u32,
1610    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1611        if count == 0 {
1612            return Ok(crate::engine::ShiftSummary::default());
1613        }
1614        if self.atomic_policy {
1615            return Err(crate::engine::EditorError::TransactionUnsupported {
1616                reason:
1617                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
1618                        .to_string(),
1619            });
1620        }
1621        self.engine.delete_rows(sheet, start, count)
1622    }
1623
1624    #[inline]
1625    pub fn insert_columns(
1626        &mut self,
1627        sheet: &str,
1628        before: u32,
1629        count: u32,
1630    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1631        if count == 0 {
1632            return Ok(crate::engine::ShiftSummary::default());
1633        }
1634        if self.capture.is_some() {
1635            let Some(capture_ptr) = self.capture else {
1636                return Err(crate::engine::EditorError::TransactionFailed {
1637                    reason: "action_atomic: missing mutation capture".to_string(),
1638                });
1639            };
1640
1641            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1642            let before0 = before.saturating_sub(1);
1643            let occupancy = self.engine.structural_column_occupancy();
1644            let affected_region = Engine::<R>::structural_col_region(sheet_id, before0);
1645
1646            let summary = {
1647                let capture = unsafe { &mut *capture_ptr };
1648                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1649                    Ok(crate::engine::ShiftSummary::default());
1650                self.engine.edit_with_capture(capture, |editor| {
1651                    editor.set_structural_occupancy(occupancy);
1652                    out = editor.insert_columns(sheet_id, before0, count);
1653                })?;
1654                out?
1655            };
1656
1657            self.engine.ensure_arrow_sheet(sheet);
1658            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1659                asheet.insert_columns(before0 as usize, count as usize);
1660            }
1661            self.engine
1662                .purge_derived_formats_after_col(sheet_id, before0);
1663            self.engine.mark_moved_formula_vertices_dirty(&summary);
1664            self.engine
1665                .clear_computed_overlay_after_col(sheet, before0 as usize);
1666            self.engine
1667                .record_structural_change(StructuralScope::Region(affected_region));
1668            if let Some(undo_ptr) = self.arrow_undo {
1669                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
1670            }
1671            Ok(summary)
1672        } else {
1673            self.engine.insert_columns(sheet, before, count)
1674        }
1675    }
1676
1677    #[inline]
1678    pub fn delete_columns(
1679        &mut self,
1680        sheet: &str,
1681        start: u32,
1682        count: u32,
1683    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1684        if count == 0 {
1685            return Ok(crate::engine::ShiftSummary::default());
1686        }
1687        if self.atomic_policy {
1688            return Err(crate::engine::EditorError::TransactionUnsupported {
1689                reason:
1690                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
1691                        .to_string(),
1692            });
1693        }
1694        self.engine.delete_columns(sheet, start, count)
1695    }
1696
1697    /// Start an action from within an action.
1698    ///
1699    /// Nested actions are currently disallowed (ticket 614), so this will return a
1700    /// `EditorError::TransactionFailed` while an outer action is active.
1701    #[inline]
1702    pub fn action<T>(
1703        &mut self,
1704        name: impl AsRef<str>,
1705        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1706    ) -> Result<T, crate::engine::EditorError> {
1707        self.engine.action(name, f)
1708    }
1709}
1710
1711struct ActionDepthGuard<'a, R> {
1712    engine: *mut Engine<R>,
1713    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
1714}
1715
1716impl<'a, R> Drop for ActionDepthGuard<'a, R> {
1717    fn drop(&mut self) {
1718        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
1719        // than the surrounding `Engine::action` call.
1720        unsafe {
1721            let e = &mut *self.engine;
1722            e.action_depth = e.action_depth.saturating_sub(1);
1723        }
1724    }
1725}
1726
1727#[derive(Default)]
1728struct SourceCache {
1729    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
1730    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
1731}
1732
1733#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1734struct VisibilityMaskCacheKey {
1735    sheet_id: SheetId,
1736    start_row0: u32,
1737    end_row0: u32,
1738    mode: VisibilityMaskMode,
1739    version: u64,
1740}
1741
1742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1743enum StructuralScope {
1744    Cell { sheet: SheetId, row: u32, col: u32 },
1745    Region(Region),
1746    Sheet(SheetId),
1747    RemovedSheet(SheetId),
1748    OpaqueGlobal,
1749    AllSheets,
1750}
1751
1752#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1753enum LoggedEditImpact {
1754    NoOp,
1755    DataOnly,
1756    Topology,
1757}
1758
1759#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1760enum LoggedEditDirection {
1761    Original,
1762    InverseReplay,
1763    ForwardReplay,
1764}
1765
1766#[derive(Clone, Copy)]
1767struct InvalidationBaseline {
1768    snapshot_id: u64,
1769    topology_epoch: u64,
1770}
1771
1772struct SourceCacheSession {
1773    cache: Arc<std::sync::RwLock<SourceCache>>,
1774}
1775
1776impl Drop for SourceCacheSession {
1777    fn drop(&mut self) {
1778        if let Ok(mut g) = self.cache.write() {
1779            *g = SourceCache::default();
1780        }
1781    }
1782}
1783
1784#[derive(Debug)]
1785#[non_exhaustive]
1786pub struct EvalResult {
1787    pub computed_vertices: usize,
1788    pub cycle_errors: usize,
1789    pub elapsed: std::time::Duration,
1790}
1791
1792#[derive(Clone, Debug, PartialEq, Eq)]
1793#[non_exhaustive]
1794pub struct TableMetadata {
1795    pub name: String,
1796    pub sheet: String,
1797    pub start_row: u32,
1798    pub start_col: u32,
1799    pub end_row: u32,
1800    pub end_col: u32,
1801    pub header_row: bool,
1802    pub headers: Vec<String>,
1803    pub totals_row: bool,
1804}
1805
1806/// Read-only engine counters used by benchmark/instrumentation tooling.
1807///
1808/// These counters are deliberately observational: collecting them must not mutate engine state or
1809/// alter formula evaluation semantics.
1810///
1811/// The `formula_plane_*` counters are always `0`: FormulaPlane spans were removed and the
1812/// dependency authority is the only runtime path. They are kept for source compatibility.
1813#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1814#[non_exhaustive]
1815pub struct EngineBaselineStats {
1816    pub graph_vertex_count: usize,
1817    pub graph_formula_vertex_count: usize,
1818    pub graph_edge_count: usize,
1819    pub dirty_vertex_count: usize,
1820    pub evaluation_vertex_count: usize,
1821    pub formula_ast_root_count: usize,
1822    pub formula_ast_node_count: usize,
1823    pub staged_formula_count: usize,
1824    pub formula_plane_active_span_count: usize,
1825    pub formula_plane_producer_result_entries: usize,
1826    pub formula_plane_consumer_read_entries: usize,
1827    pub formula_plane_mixed_topology_cache_builds: u64,
1828    pub formula_plane_mixed_topology_cache_hits: u64,
1829    pub formula_plane_mixed_topology_cache_overflows: u64,
1830    pub formula_plane_dirty_pending_events: usize,
1831    pub formula_plane_dirty_region_events_recorded: u64,
1832    pub formula_plane_dirty_span_region_events_recorded: u64,
1833    pub formula_plane_dirty_whole_span_seeds_recorded: u64,
1834    pub formula_plane_dirty_global_invalidations: u64,
1835    pub formula_plane_structural_span_candidates: u64,
1836    pub formula_plane_cycle_member_span_demotions: u64,
1837    pub formula_plane_array_result_span_demotions: u64,
1838    /// Members of exactly converged iterative SCCs currently retained across
1839    /// recalcs (#368).
1840    pub retained_scc_members: usize,
1841}
1842
1843#[derive(Debug, Clone, Default)]
1844#[non_exhaustive]
1845pub struct VirtualDepTelemetry {
1846    pub candidate_vertices_total: usize,
1847    pub vdeps_vertices_total: usize,
1848    pub vdeps_edges_total: usize,
1849    pub builder_elapsed_ms_total: u128,
1850    pub schedule_virtual_passes: usize,
1851    pub schedule_static_passes: usize,
1852    pub schedule_cache_hits: usize,
1853    pub schedule_cache_misses: usize,
1854    pub reused_schedule_vertices_total: usize,
1855    pub replan_iterations: usize,
1856    pub changed_vdeps_total: usize,
1857    pub bailout_reason: Option<&'static str>,
1858    pub fallback_mode_activations: u64,
1859}
1860
1861/// Per-recalc telemetry for SCC evaluation under `CycleDetection::Runtime`
1862/// (spec `formualizer-cycle-semantics-spec.md` §10).
1863///
1864/// Collection is unconditional: SCC tasks are rare relative to ordinary
1865/// vertex evaluation and the counters are a handful of integer adds per
1866/// task, so no config flag gates them (unlike [`VirtualDepTelemetry`],
1867/// which pays per-schedule costs). Counters reset at the start of every
1868/// evaluation request.
1869#[derive(Debug, Clone, Default, PartialEq)]
1870#[non_exhaustive]
1871pub struct CycleTelemetry {
1872    /// SCC tasks executed (static SCCs that reached Runtime evaluation).
1873    pub static_sccs: usize,
1874    /// SCC tasks whose live subgraph was acyclic — values produced.
1875    pub phantom_sccs: usize,
1876    /// Distinct live cycles witnessed across all SCC tasks.
1877    pub live_cycles_witnessed: usize,
1878    /// Cells stamped `#CIRC!` by Runtime SCC tasks.
1879    pub circ_cells_stamped: usize,
1880    /// Evaluation sweeps over (subsets of) SCC members, totalled across tasks
1881    /// (pass 1 included).
1882    pub settle_passes_total: usize,
1883    /// Largest pass count any single SCC task needed.
1884    pub max_passes_single_scc: usize,
1885    /// SCC tasks that entered iterative calculation (`CyclePolicy::Iterate`
1886    /// with a witnessed live cycle). RFC #113, Stage 3.
1887    pub iterated_sccs: usize,
1888    /// Iterating SCC tasks that stopped because every member passed the
1889    /// spec-§6 convergence test.
1890    pub converged_sccs: usize,
1891    /// SCC tasks that stopped at a pass cap. Under `CyclePolicy::Iterate`
1892    /// this is the Excel `max_iterations` cap (NOT an error — last values
1893    /// are kept; includes the no-convergence-test `max_iterations: 1`
1894    /// contract). Under `CyclePolicy::Error` it is the defensive acyclic
1895    /// settle cap (|SCC| + 2), which only a bug can hit.
1896    pub capped_sccs: usize,
1897    /// Largest `|Δ|` observed in any member's final-pass convergence
1898    /// comparison across iterating SCC tasks (numeric-class members only).
1899    /// `0.0` when no comparison ran (e.g. `max_iterations: 1`).
1900    pub max_abs_delta_at_stop: f64,
1901    /// Identical-bit NaN vs NaN member comparisons that were treated as
1902    /// converged (spec §6 NaN rule).
1903    pub nan_converged: usize,
1904    /// Retained iterative SCCs (#368) that had no dirty member at request begin and were therefore not
1905    /// re-run: their last exact fixed point is served as-is. Counted at
1906    /// request begin, so a demand-driven request that never reaches a
1907    /// retained SCC still reports it as reused.
1908    pub reused_sccs: usize,
1909    /// Members of the SCCs counted in `reused_sccs`.
1910    pub reused_scc_members: usize,
1911    /// Total wall-clock time spent inside Runtime SCC tasks.
1912    pub elapsed_ms: u128,
1913}
1914
1915#[derive(Debug, Clone, Copy)]
1916struct ScheduleBuildMeta {
1917    candidate_vertices: usize,
1918    vdeps_vertices: usize,
1919    vdeps_edges: usize,
1920    builder_elapsed_ms: u128,
1921    used_virtual_schedule: bool,
1922    schedule_cache_hit: bool,
1923    schedule_cache_eligible: bool,
1924}
1925
1926#[cfg(any(test, feature = "benchmark_internal"))]
1927#[doc(hidden)]
1928#[derive(Debug, Clone, Default)]
1929pub struct RecalcReuseProbe {
1930    pub schedule_requests: usize,
1931    pub schedule_cache_hits: usize,
1932    pub schedule_cache_misses: usize,
1933    pub schedule_cache_ineligible: usize,
1934    pub schedule_builds: usize,
1935    /// Program 3 plan reuse: misses served by restricting the base schedule.
1936    pub schedule_base_restrictions: usize,
1937    pub schedule_shared_handles: usize,
1938    pub schedule_retained_bytes: usize,
1939    pub legacy_target_requests: usize,
1940    pub target_schedule_builds: usize,
1941    pub demand_builds: usize,
1942    pub demand_vertices: usize,
1943    pub demand_clean_formulas: usize,
1944    pub demand_explicit_edges: usize,
1945    pub demand_virtual_builder_calls: usize,
1946}
1947
1948#[cfg(any(test, feature = "benchmark_internal"))]
1949fn schedule_probe_retained_bytes(schedule: &crate::engine::Schedule) -> usize {
1950    fn vector_bytes<T>(values: &Vec<T>) -> usize {
1951        values.capacity() * std::mem::size_of::<T>()
1952    }
1953
1954    [
1955        vector_bytes(&schedule.units),
1956        vector_bytes(&schedule.layers),
1957        vector_bytes(&schedule.cycles),
1958    ]
1959    .into_iter()
1960    .chain(
1961        schedule
1962            .layers
1963            .iter()
1964            .map(|layer| vector_bytes(&layer.vertices)),
1965    )
1966    .chain(schedule.cycles.iter().map(vector_bytes))
1967    .sum()
1968}
1969
1970#[derive(Debug, Clone)]
1971struct CachedScheduleEntry {
1972    topology_epoch: u64,
1973    /// Authority `(store revision, rev.dyn)` the schedule was planned from
1974    /// (design §8.4; always 0 without `unified_authority`).
1975    authority_revision: (u64, u64),
1976    /// The request's vertex list as runs of consecutive ids (formula ids
1977    /// come in column runs, so a whole-workbook request is a few runs).
1978    candidate_vertices: VertexIdRuns,
1979    schedule: Arc<crate::engine::scheduler::Schedule>,
1980}
1981
1982/// A vertex list stored as `(first id, run length)` runs of consecutive
1983/// ids, in list order.
1984#[derive(Debug, Clone, Default)]
1985struct VertexIdRuns(Vec<(u32, u32)>);
1986
1987impl VertexIdRuns {
1988    fn from_slice(ids: &[VertexId]) -> Self {
1989        let mut runs: Vec<(u32, u32)> = Vec::new();
1990        for v in ids {
1991            match runs.last_mut() {
1992                Some((first, len)) if first.checked_add(*len) == Some(v.0) => *len += 1,
1993                _ => runs.push((v.0, 1)),
1994            }
1995        }
1996        runs.shrink_to_fit();
1997        Self(runs)
1998    }
1999
2000    fn equals(&self, ids: &[VertexId]) -> bool {
2001        let mut rest = ids;
2002        for &(first, len) in &self.0 {
2003            let len = len as usize;
2004            if rest.len() < len {
2005                return false;
2006            }
2007            let (head, tail) = rest.split_at(len);
2008            if head
2009                .iter()
2010                .enumerate()
2011                .any(|(i, v)| v.0 != first.wrapping_add(i as u32))
2012            {
2013                return false;
2014            }
2015            rest = tail;
2016        }
2017        rest.is_empty()
2018    }
2019
2020    fn heap_bytes(&self) -> usize {
2021        self.0.capacity() * std::mem::size_of::<(u32, u32)>()
2022    }
2023
2024    /// The number of ids.
2025    fn len(&self) -> usize {
2026        self.0.iter().map(|&(_, len)| len as usize).sum()
2027    }
2028}
2029
2030#[cfg(test)]
2031mod vertex_id_runs_tests {
2032    use super::{VertexId, VertexIdRuns};
2033
2034    #[test]
2035    fn runs_compare_like_the_list() {
2036        let ids = |v: &[u32]| v.iter().map(|&i| VertexId(i)).collect::<Vec<_>>();
2037        let list = ids(&[5, 6, 7, 2, 3, 9, 10, 10]);
2038        let runs = VertexIdRuns::from_slice(&list);
2039        assert_eq!(runs.0, vec![(5, 3), (2, 2), (9, 2), (10, 1)]);
2040        assert!(runs.equals(&list));
2041        assert!(!runs.equals(&list[..7]));
2042        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 11])));
2043        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 10, 11])));
2044        assert!(VertexIdRuns::from_slice(&[]).equals(&[]));
2045        assert!(!VertexIdRuns::from_slice(&[]).equals(&list));
2046        let edge = ids(&[u32::MAX - 1, u32::MAX, 0]);
2047        assert!(VertexIdRuns::from_slice(&edge).equals(&edge));
2048    }
2049}
2050
2051/// Uncacheable requests keep their schedule inline without a shared allocation.
2052enum EvaluationSchedule {
2053    Owned(crate::engine::scheduler::Schedule),
2054    Shared(Arc<crate::engine::scheduler::Schedule>),
2055}
2056
2057impl std::ops::Deref for EvaluationSchedule {
2058    type Target = crate::engine::scheduler::Schedule;
2059
2060    fn deref(&self) -> &Self::Target {
2061        match self {
2062            Self::Owned(schedule) => schedule,
2063            Self::Shared(schedule) => schedule,
2064        }
2065    }
2066}
2067
2068type ScheduleBuildOutput = (
2069    crate::engine::scheduler::Schedule,
2070    FxHashMap<VertexId, Vec<VertexId>>,
2071    ScheduleBuildMeta,
2072);
2073
2074type EvaluationScheduleBuildOutput = (
2075    EvaluationSchedule,
2076    FxHashMap<VertexId, Vec<VertexId>>,
2077    ScheduleBuildMeta,
2078);
2079
2080/// Opaque, revision-bound recalculation recipe.
2081#[derive(Debug)]
2082pub struct RecalcPlan {
2083    key: RecalcPlanKey,
2084    kind: RecalcPlanKind,
2085}
2086
2087#[derive(Debug)]
2088struct RecalcPlanKey {
2089    engine_token: Arc<()>,
2090    revisions: PlanningRevisionSnapshot,
2091}
2092
2093#[derive(Clone, Debug, PartialEq, Eq)]
2094struct PlanningRevisionSnapshot {
2095    engine_topology_epoch: u64,
2096    graph_topology_revision: u64,
2097    staged: u64,
2098    symbols: u64,
2099    semantic: u64,
2100    provider: Option<u64>,
2101    deterministic_mode: crate::engine::DeterministicMode,
2102    budgets: crate::engine::EvaluationBudgets,
2103}
2104
2105#[derive(Debug)]
2106enum RecalcPlanKind {
2107    CompatibilityFull {
2108        schedule: crate::engine::Schedule,
2109        has_dynamic_refs: bool,
2110    },
2111    Target {
2112        targets: Vec<crate::engine::EvaluationTarget>,
2113        scope: crate::engine::PrepareScope,
2114        topology: RecalcTopology,
2115        dynamic_policy: DynamicPlanPolicy,
2116    },
2117}
2118
2119#[derive(Debug)]
2120enum RecalcTopology {
2121    RunLocalRecipe,
2122    Workbook,
2123}
2124
2125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2126enum DynamicPlanPolicy {
2127    BoundedTargetReplan,
2128}
2129
2130impl RecalcPlan {
2131    /// Returns the retained compatibility schedule depth. Target plans retain a
2132    /// run-local recipe rather than a schedule, so their layer count is zero.
2133    pub fn layer_count(&self) -> usize {
2134        match &self.kind {
2135            RecalcPlanKind::CompatibilityFull { schedule, .. } => schedule.layers.len(),
2136            RecalcPlanKind::Target { .. } => 0,
2137        }
2138    }
2139
2140    pub fn has_dynamic_refs(&self) -> bool {
2141        match &self.kind {
2142            RecalcPlanKind::CompatibilityFull {
2143                has_dynamic_refs, ..
2144            } => *has_dynamic_refs,
2145            RecalcPlanKind::Target { .. } => false,
2146        }
2147    }
2148
2149    #[cfg(test)]
2150    pub(crate) fn force_stale_reasons_for_test(
2151        &mut self,
2152        reasons: &[formualizer_common::PlanStaleReason],
2153    ) {
2154        use formualizer_common::PlanStaleReason;
2155        for reason in reasons {
2156            match reason {
2157                PlanStaleReason::Engine => {
2158                    self.key.engine_token = Arc::new(());
2159                }
2160                PlanStaleReason::Provider => {
2161                    self.key.revisions.provider = Some(
2162                        self.key
2163                            .revisions
2164                            .provider
2165                            .unwrap_or_default()
2166                            .wrapping_add(1),
2167                    );
2168                }
2169                PlanStaleReason::Semantic => {
2170                    self.key.revisions.semantic = self.key.revisions.semantic.wrapping_add(1);
2171                }
2172                PlanStaleReason::Budget => {
2173                    let current = self.key.revisions.budgets.work.max_work_units;
2174                    self.key.revisions.budgets.work.max_work_units =
2175                        Some(current.unwrap_or_default().wrapping_add(1));
2176                }
2177                PlanStaleReason::Staged => {
2178                    self.key.revisions.staged = self.key.revisions.staged.wrapping_add(1);
2179                }
2180                PlanStaleReason::Symbols => {
2181                    self.key.revisions.symbols = self.key.revisions.symbols.wrapping_add(1);
2182                }
2183                PlanStaleReason::Graph => {
2184                    self.key.revisions.graph_topology_revision =
2185                        self.key.revisions.graph_topology_revision.wrapping_add(1);
2186                }
2187                _ => {}
2188            }
2189        }
2190    }
2191}
2192
2193#[cfg(any(test, feature = "test-support"))]
2194pub(crate) mod criteria_mask_test_hooks {
2195    use std::cell::Cell;
2196
2197    thread_local! {
2198        static MASK_CALLS_ROWS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
2199        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
2200        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
2201    }
2202
2203    pub(crate) fn take_mask_work() -> (usize, usize) {
2204        MASK_CALLS_ROWS.with(|c| c.replace((0, 0)))
2205    }
2206
2207    pub(crate) fn note_mask(rows: usize) {
2208        MASK_CALLS_ROWS.with(|c| {
2209            let (calls, work) = c.get();
2210            c.set((calls + 1, work + rows));
2211        });
2212    }
2213
2214    pub fn reset_text_segment_counters() {
2215        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
2216        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
2217    }
2218
2219    pub fn text_segment_counters() -> (usize, usize) {
2220        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
2221        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
2222        (a, b)
2223    }
2224
2225    pub(crate) fn inc_total() {
2226        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
2227    }
2228    pub(crate) fn inc_all_null() {
2229        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
2230    }
2231}
2232
2233#[cfg(test)]
2234pub(crate) mod visibility_mask_test_hooks {
2235    use std::cell::Cell;
2236
2237    thread_local! {
2238        static HITS: Cell<usize> = const { Cell::new(0) };
2239        static MISSES: Cell<usize> = const { Cell::new(0) };
2240        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
2241    }
2242
2243    pub fn reset() {
2244        HITS.with(|c| c.set(0));
2245        MISSES.with(|c| c.set(0));
2246        EVICTIONS.with(|c| c.set(0));
2247    }
2248
2249    pub fn counters() -> (usize, usize, usize) {
2250        let hits = HITS.with(|c| c.get());
2251        let misses = MISSES.with(|c| c.get());
2252        let evictions = EVICTIONS.with(|c| c.get());
2253        (hits, misses, evictions)
2254    }
2255
2256    pub(crate) fn inc_hit() {
2257        HITS.with(|c| c.set(c.get() + 1));
2258    }
2259
2260    pub(crate) fn inc_miss() {
2261        MISSES.with(|c| c.set(c.get() + 1));
2262    }
2263
2264    pub(crate) fn inc_eviction() {
2265        EVICTIONS.with(|c| c.set(c.get() + 1));
2266    }
2267}
2268
2269fn is_numeric_text_equality(pred: &crate::args::CriteriaPredicate) -> bool {
2270    match pred {
2271        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(text)) => {
2272            text.trim().parse::<f64>().is_ok_and(f64::is_finite)
2273        }
2274        _ => false,
2275    }
2276}
2277
2278fn compute_criteria_mask(
2279    view: &RangeView<'_>,
2280    col_in_view: usize,
2281    pred: &crate::args::CriteriaPredicate,
2282) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2283    use crate::compute_prelude::{boolean, cmp, concat_arrays};
2284    use arrow::compute::kernels::comparison::{ilike, nilike};
2285    use arrow_array::{
2286        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
2287    };
2288
2289    // Helper: apply a numeric predicate to a single Float64Array chunk
2290    fn apply_numeric_pred(
2291        chunk: &Float64Array,
2292        pred: &crate::args::CriteriaPredicate,
2293    ) -> Option<BooleanArray> {
2294        match pred {
2295            crate::args::CriteriaPredicate::Gt(n) => {
2296                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
2297            }
2298            crate::args::CriteriaPredicate::Ge(n) => {
2299                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2300            }
2301            crate::args::CriteriaPredicate::Lt(n) => {
2302                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
2303            }
2304            crate::args::CriteriaPredicate::Le(n) => {
2305                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2306            }
2307            crate::args::CriteriaPredicate::Eq(v) => match v {
2308                formualizer_common::LiteralValue::Number(x) => {
2309                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
2310                }
2311                formualizer_common::LiteralValue::Int(i) => {
2312                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2313                }
2314                _ => None,
2315            },
2316            crate::args::CriteriaPredicate::Ne(v) => match v {
2317                formualizer_common::LiteralValue::Number(x) => {
2318                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
2319                }
2320                formualizer_common::LiteralValue::Int(i) => {
2321                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2322                }
2323                _ => None,
2324            },
2325            _ => None,
2326        }
2327    }
2328
2329    // Check if this is a numeric predicate that can be applied per-chunk
2330    let is_numeric_pred = matches!(
2331        pred,
2332        crate::args::CriteriaPredicate::Gt(_)
2333            | crate::args::CriteriaPredicate::Ge(_)
2334            | crate::args::CriteriaPredicate::Lt(_)
2335            | crate::args::CriteriaPredicate::Le(_)
2336            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
2337            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
2338            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
2339            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
2340    );
2341
2342    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
2343    // This avoids materializing the full numeric column (64-bit per element) and instead
2344    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
2345    if is_numeric_pred {
2346        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2347        for res in view.numbers_slices() {
2348            let (_rs, _rl, cols_seg) = res.ok()?;
2349            if col_in_view < cols_seg.len() {
2350                let chunk = cols_seg[col_in_view].as_ref();
2351                let mask = apply_numeric_pred(chunk, pred)?;
2352                bool_parts.push(mask);
2353            }
2354        }
2355
2356        if bool_parts.is_empty() {
2357            return None;
2358        } else if bool_parts.len() == 1 {
2359            return Some(std::sync::Arc::new(bool_parts.remove(0)));
2360        } else {
2361            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
2362            let anys: Vec<&dyn arrow_array::Array> = bool_parts
2363                .iter()
2364                .map(|a| a as &dyn arrow_array::Array)
2365                .collect();
2366            let conc: ArrayRef = concat_arrays(&anys).ok()?;
2367            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2368            return Some(std::sync::Arc::new(ba));
2369        }
2370    }
2371
2372    // Wildcards and numeric text equality can match non-text cells. The lowered
2373    // base lane is text-only, unlike the scalar matcher. Keep the vectorized
2374    // path for text-only data, but cache a scalar-equivalent mask for mixed data.
2375    if is_numeric_text_equality(pred)
2376        || matches!(pred, crate::args::CriteriaPredicate::TextLike { .. })
2377    {
2378        for tags in view.type_tags_slices() {
2379            let (_, _, cols) = tags.ok()?;
2380            let tags = cols.get(col_in_view)?;
2381            if tags.values().iter().any(|tag| {
2382                *tag == crate::arrow_store::TypeTag::Empty as u8
2383                    || *tag == crate::arrow_store::TypeTag::Number as u8
2384                    || *tag == crate::arrow_store::TypeTag::Boolean as u8
2385            }) {
2386                let mut mask = BooleanBuilder::new();
2387                for chunk in view.iter_row_chunks() {
2388                    let chunk = chunk.ok()?;
2389                    for row in chunk.row_start..chunk.row_start + chunk.row_len {
2390                        mask.append_value(crate::builtins::criteria_match(
2391                            pred,
2392                            &view.get_cell(row, col_in_view),
2393                        ));
2394                    }
2395                }
2396                return Some(std::sync::Arc::new(mask.finish()));
2397            }
2398        }
2399    }
2400
2401    // SQL LIKE cannot directly represent spreadsheet tilde escapes or literal
2402    // SQL pattern punctuation. Let the bounded chunk fallback use the shared
2403    // spreadsheet matcher rather than rewriting these patterns into SQL syntax.
2404    if matches!(pred, crate::args::CriteriaPredicate::TextLike { pattern, .. }
2405        if pattern.contains(['~', '%', '_', '\\']))
2406    {
2407        return None;
2408    }
2409
2410    // TEXT PATH: build masks per row-chunk using lowered text slices.
2411    // This avoids concatenating full-string columns just to compute a boolean mask.
2412    let (text_kind, text_pat, empty_special) = match pred {
2413        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
2414            (0u8, t.to_lowercase(), t.is_empty())
2415        }
2416        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
2417            (1u8, t.to_lowercase(), false)
2418        }
2419        crate::args::CriteriaPredicate::TextLike {
2420            pattern,
2421            case_insensitive,
2422        } => {
2423            let p = if *case_insensitive {
2424                pattern.to_lowercase()
2425            } else {
2426                pattern.clone()
2427            };
2428            (2u8, p.replace('*', "%").replace('?', "_"), false)
2429        }
2430        _ => return None,
2431    };
2432
2433    let text_pat_is_empty = text_pat.is_empty();
2434    let ne_matches_blank = text_kind == 1 && !text_pat_is_empty;
2435    let pat = StringArray::new_scalar(text_pat);
2436    let mut bool_parts: Vec<BooleanArray> = Vec::new();
2437
2438    let mut tag_slices = view.type_tags_slices();
2439    for res in view.iter_row_chunks() {
2440        let cs = res.ok()?;
2441        if cs.row_len == 0 {
2442            continue;
2443        }
2444        #[cfg(test)]
2445        criteria_mask_test_hooks::inc_total();
2446
2447        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
2448        if col_in_view >= slices.len() {
2449            return None;
2450        }
2451
2452        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
2453        if empty_special || (text_kind == 1 && text_pat_is_empty) {
2454            let (tag_start, tag_len, tags) = tag_slices.next()?.ok()?;
2455            if tag_start != cs.row_start || tag_len != cs.row_len {
2456                return None;
2457            }
2458            let tags = tags.get(col_in_view)?;
2459            // A null text lane is not a blank cell: base numeric/boolean/error
2460            // cells also have null text. Consult the overlay-aware type tags,
2461            // and inspect strings only to distinguish empty text from text.
2462            let strings = seg_opt.and_then(|a| a.as_any().downcast_ref::<StringArray>());
2463            let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2464            for i in 0..cs.row_len {
2465                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8
2466                    || (tags.value(i) == crate::arrow_store::TypeTag::Text as u8
2467                        && strings.is_some_and(|s| s.is_valid(i) && s.value(i).is_empty()));
2468                bb.append_value(if text_kind == 0 { blank } else { !blank });
2469            }
2470            #[cfg(test)]
2471            if seg_opt.is_none() {
2472                criteria_mask_test_hooks::inc_all_null();
2473            }
2474            bool_parts.push(bb.finish());
2475            continue;
2476        }
2477        let seg = match seg_opt {
2478            Some(s) => s,
2479            None => {
2480                #[cfg(test)]
2481                criteria_mask_test_hooks::inc_all_null();
2482                if (text_kind == 0 && empty_special) || ne_matches_blank {
2483                    // Eq("") treats nulls (Empty) as equal.
2484                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2485                    bb.append_n(cs.row_len, true);
2486                    bool_parts.push(bb.finish());
2487                } else {
2488                    // For non-empty patterns, ilike/nilike return null on null inputs.
2489                    bool_parts.push(BooleanArray::new_null(cs.row_len));
2490                }
2491                continue;
2492            }
2493        };
2494
2495        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
2496        let mut m = match text_kind {
2497            0 => ilike(seg_sa, &pat).ok()?,
2498            1 => nilike(seg_sa, &pat).ok()?,
2499            2 => ilike(seg_sa, &pat).ok()?,
2500            _ => return None,
2501        };
2502
2503        // Only fold blank/Empty (null) cells into the mask when the segment
2504        // actually contains any. The null-fill loop + or_kleene are pure
2505        // overhead on blank-free chunks, so a `<>text` (or `=""`) aggregation
2506        // over a column with no blanks stays fully vectorized on the ilike/
2507        // nilike result.
2508        if ((text_kind == 0 && empty_special) || ne_matches_blank) && seg_sa.null_count() > 0 {
2509            // Treat nulls as equal to empty string
2510            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
2511            for i in 0..seg_sa.len() {
2512                bb.append_value(seg_sa.is_null(i));
2513            }
2514            let nulls = bb.finish();
2515            m = boolean::or_kleene(&m, &nulls).ok()?;
2516        }
2517
2518        bool_parts.push(m);
2519    }
2520
2521    if bool_parts.is_empty() {
2522        None
2523    } else if bool_parts.len() == 1 {
2524        Some(std::sync::Arc::new(bool_parts.remove(0)))
2525    } else {
2526        let anys: Vec<&dyn arrow_array::Array> = bool_parts
2527            .iter()
2528            .map(|a| a as &dyn arrow_array::Array)
2529            .collect();
2530        let conc: ArrayRef = concat_arrays(&anys).ok()?;
2531        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2532        Some(std::sync::Arc::new(ba))
2533    }
2534}
2535
2536#[derive(Debug, Clone)]
2537pub struct LayerInfo {
2538    pub vertex_count: usize,
2539    pub parallel_eligible: bool,
2540    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
2541}
2542
2543#[derive(Debug, Clone)]
2544pub struct EvalPlan {
2545    pub total_vertices_to_evaluate: usize,
2546    pub layers: Vec<LayerInfo>,
2547    pub cycles_detected: usize,
2548    pub dirty_count: usize,
2549    pub volatile_count: usize,
2550    pub parallel_enabled: bool,
2551    pub estimated_parallel_layers: usize,
2552    pub target_cells: Vec<String>,
2553}
2554
2555/// Whether the configured FormulaPlane mode is ignored (spans never placed):
2556/// always, since the dependency authority is the runtime path (design §10).
2557#[inline]
2558fn plane_mode_ignored() -> bool {
2559    true
2560}
2561
2562/// Test-support probe for sibling-crate tests: true when span placement is
2563/// off because the FormulaPlane mode is ignored (see `plane_mode_ignored`).
2564#[cfg(feature = "test-support")]
2565#[doc(hidden)]
2566pub fn formula_plane_mode_ignored_for_test() -> bool {
2567    plane_mode_ignored()
2568}
2569
2570impl<R> Engine<R>
2571where
2572    R: EvaluationContext,
2573{
2574    /// # Panics
2575    /// Panics when `config.cycle` is invalid ([`CycleConfig::validate`],
2576    /// spec §2): `Iterate` with `detection: Static`, `max_iterations == 0`,
2577    /// or a negative/non-finite `max_change`. `EvalConfig::with_cycle`
2578    /// rejects these at build; this re-validates configs assembled via
2579    /// struct literals.
2580    pub fn new(resolver: R, config: EvalConfig) -> Self {
2581        // Under the unified authority the FormulaPlane mode is accepted and
2582        // ignored (design §10). Normalizing the stored mode keeps external
2583        // readers of `config` (loaders choosing a span-preparation route) on
2584        // the per-cell path; engine reads go through `formula_plane_mode()`.
2585        let config = if plane_mode_ignored() {
2586            EvalConfig {
2587                formula_plane_mode: FormulaPlaneMode::Off,
2588                ..config
2589            }
2590        } else {
2591            config
2592        };
2593        if let Err(msg) = config.cycle.validate() {
2594            panic!("invalid CycleConfig: {msg}");
2595        }
2596        crate::builtins::load_builtins();
2597        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2598            &config.evaluation_budgets,
2599            config.max_vertices,
2600            config.max_memory_mb,
2601            config.max_eval_time,
2602        );
2603
2604        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2605            #[cfg(feature = "system-clock")]
2606            {
2607                Arc::new(crate::timezone::SystemClock::new(
2608                    crate::timezone::TimeZoneSpec::default(),
2609                ))
2610            }
2611            #[cfg(not(feature = "system-clock"))]
2612            {
2613                Arc::new(crate::timezone::FixedClock::new(
2614                    chrono::DateTime::UNIX_EPOCH,
2615                    crate::timezone::TimeZoneSpec::Utc,
2616                ))
2617            }
2618        });
2619
2620        // Initialize thread pool based on config
2621        let thread_pool = if config.enable_parallel {
2622            let mut builder = ThreadPoolBuilder::new();
2623            if let Some(max_threads) = config.max_threads {
2624                builder = builder.num_threads(max_threads);
2625            }
2626
2627            match builder.build() {
2628                Ok(pool) => Some(Arc::new(pool)),
2629                Err(_) => {
2630                    // Fall back to sequential evaluation if thread pool creation fails
2631                    None
2632                }
2633            }
2634        } else {
2635            None
2636        };
2637
2638        // C1a retained/cache budgets are observational; cache defaults stay explicit.
2639        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
2640        let function_provider_revision_seen = resolver.planning_semantic_revision();
2641        let mut engine = Self {
2642            graph: DependencyGraph::new_with_config(config.clone()),
2643            resolver,
2644            config,
2645            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
2646            clock: crate::timezone::SnapshotClock::new(clock),
2647            thread_pool,
2648            recalc_epoch: 0,
2649            snapshot_id: std::sync::atomic::AtomicU64::new(1),
2650            topology_epoch: 0,
2651            cached_static_schedule: None,
2652            recent_schedules: Vec::new(),
2653            base_schedule: None,
2654            #[cfg(any(test, feature = "benchmark_internal"))]
2655            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
2656            spill_mgr: ShimSpillManager::default(),
2657            arrow_sheets: SheetStore::default(),
2658            format_registry: crate::format::FormatRegistry::default(),
2659            derived_formats: Default::default(),
2660            #[cfg(test)]
2661            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
2662            #[cfg(test)]
2663            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
2664            #[cfg(test)]
2665            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
2666            #[cfg(test)]
2667            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
2668            #[cfg(test)]
2669            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
2670            #[cfg(test)]
2671            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
2672            #[cfg(test)]
2673            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
2674            #[cfg(test)]
2675            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
2676            compressed_at_build: None,
2677            #[cfg(test)]
2678            computed_overlay_set_explicit_entry_operations_for_test: 0,
2679            #[cfg(test)]
2680            computed_overlay_stale_clear_range_effects_for_test: 0,
2681            #[cfg(test)]
2682            computed_overlay_stale_clear_offset_attempts_for_test: 0,
2683            #[cfg(test)]
2684            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
2685            has_edited: false,
2686            overlay_compactions: 0,
2687            computed_overlay_bytes_estimate: 0,
2688            computed_overlay_mirroring_disabled: false,
2689            force_materialize_range_views: false,
2690            row_bounds_cache: std::sync::RwLock::new(None),
2691            used_axis_bounds_cache: std::sync::RwLock::new(None),
2692            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
2693            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
2694            source_formula_token: Arc::new(()),
2695            recalc_plan_token: Arc::new(()),
2696            staged_formulas: std::collections::HashMap::new(),
2697            staged_formula_index: StagedFormulaIndex::default(),
2698            blocked_pending_spills: Vec::new(),
2699            row_visibility: FxHashMap::default(),
2700            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
2701            formula_parse_diagnostics: Vec::new(),
2702            last_formula_ingest_report: None,
2703            formula_ingest_report_total: FormulaIngestReport::default(),
2704            active_cancel_flag: None,
2705            active_evaluation_deadline: None,
2706            action_depth: 0,
2707            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
2708            virtual_dep_fallback_activations: 0,
2709            last_cycle_telemetry: CycleTelemetry::default(),
2710            next_evaluation_resource_request_id: 1,
2711            evaluation_resource_request_depth: 0,
2712            active_evaluation_resource_request: None,
2713            last_evaluation_resource_request: None,
2714            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
2715            evaluation_resource_request_started_at: None,
2716            evaluation_resource_budgets: resolved_resources.budgets,
2717            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
2718            active_resource_ledger: None,
2719            source_cache_footprints: Vec::new(),
2720            source_cache_accounted: 0,
2721            pending_iterative_redirty: Vec::new(),
2722            retained_scc_members: FxHashMap::default(),
2723            next_retained_scc_id: 0,
2724            retained_scc_config_fingerprint: 0,
2725            retained_scc_function_epoch_seen: 0,
2726            retained_scc_provider_revision_seen: None,
2727            retained_scc_dirty_at_begin: Vec::new(),
2728            iterative_state_values: FxHashMap::default(),
2729            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
2730            function_provider_revision_seen,
2731            #[cfg(feature = "tracing")]
2732            trace_evaluation_counters: TraceEvaluationCounters::default(),
2733            #[cfg(test)]
2734            evaluation_request_begin_count_for_test: 0,
2735            #[cfg(any(test, feature = "test-support"))]
2736            before_prepared_span_commit_hook: None,
2737            #[cfg(test)]
2738            before_target_preparation_commit_hook: None,
2739            #[cfg(test)]
2740            before_target_planning_snapshot_hook: None,
2741            #[cfg(test)]
2742            inject_target_semantic_stale_once_for_test: false,
2743            #[cfg(test)]
2744            force_virtual_dep_changes_remaining_for_test: 0,
2745            freshness: Default::default(),
2746            #[cfg(test)]
2747            fail_evaluation_commit_preflight_once_for_test: false,
2748            #[cfg(test)]
2749            target_preparation_fault_for_test: None,
2750            #[cfg(test)]
2751            force_non_cycle_schedule_fallback_for_test: false,
2752            #[cfg(test)]
2753            before_legacy_fallback_final_provider_sample_hook: None,
2754            #[cfg(test)]
2755            after_eager_proposal_commit_hook: None,
2756        };
2757        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
2758        engine.config.arrow_storage_enabled = true;
2759        engine.config.delta_overlay_enabled = true;
2760        engine.config.write_formula_overlay_enabled = true;
2761        let default_sheet = engine.graph.default_sheet_name().to_string();
2762        engine.ensure_arrow_sheet(&default_sheet);
2763        engine
2764    }
2765
2766    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
2767    ///
2768    /// # Panics
2769    /// Panics when `config.cycle` is invalid, exactly like [`Engine::new`].
2770    pub fn with_thread_pool(
2771        resolver: R,
2772        config: EvalConfig,
2773        thread_pool: Arc<rayon::ThreadPool>,
2774    ) -> Self {
2775        if let Err(msg) = config.cycle.validate() {
2776            panic!("invalid CycleConfig: {msg}");
2777        }
2778        crate::builtins::load_builtins();
2779        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2780            &config.evaluation_budgets,
2781            config.max_vertices,
2782            config.max_memory_mb,
2783            config.max_eval_time,
2784        );
2785        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2786            #[cfg(feature = "system-clock")]
2787            {
2788                Arc::new(crate::timezone::SystemClock::new(
2789                    crate::timezone::TimeZoneSpec::default(),
2790                ))
2791            }
2792            #[cfg(not(feature = "system-clock"))]
2793            {
2794                Arc::new(crate::timezone::FixedClock::new(
2795                    chrono::DateTime::UNIX_EPOCH,
2796                    crate::timezone::TimeZoneSpec::Utc,
2797                ))
2798            }
2799        });
2800        // C1a retained/cache budgets are observational; cache defaults stay explicit.
2801        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
2802        let function_provider_revision_seen = resolver.planning_semantic_revision();
2803        let mut engine = Self {
2804            graph: DependencyGraph::new_with_config(config.clone()),
2805            resolver,
2806            config,
2807            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
2808            clock: crate::timezone::SnapshotClock::new(clock),
2809            thread_pool: Some(thread_pool),
2810            recalc_epoch: 0,
2811            snapshot_id: std::sync::atomic::AtomicU64::new(1),
2812            topology_epoch: 0,
2813            cached_static_schedule: None,
2814            recent_schedules: Vec::new(),
2815            base_schedule: None,
2816            #[cfg(any(test, feature = "benchmark_internal"))]
2817            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
2818            spill_mgr: ShimSpillManager::default(),
2819            arrow_sheets: SheetStore::default(),
2820            format_registry: crate::format::FormatRegistry::default(),
2821            derived_formats: Default::default(),
2822            #[cfg(test)]
2823            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
2824            #[cfg(test)]
2825            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
2826            #[cfg(test)]
2827            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
2828            #[cfg(test)]
2829            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
2830            #[cfg(test)]
2831            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
2832            #[cfg(test)]
2833            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
2834            #[cfg(test)]
2835            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
2836            #[cfg(test)]
2837            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
2838            compressed_at_build: None,
2839            #[cfg(test)]
2840            computed_overlay_set_explicit_entry_operations_for_test: 0,
2841            #[cfg(test)]
2842            computed_overlay_stale_clear_range_effects_for_test: 0,
2843            #[cfg(test)]
2844            computed_overlay_stale_clear_offset_attempts_for_test: 0,
2845            #[cfg(test)]
2846            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
2847            has_edited: false,
2848            overlay_compactions: 0,
2849            computed_overlay_bytes_estimate: 0,
2850            computed_overlay_mirroring_disabled: false,
2851            force_materialize_range_views: false,
2852            row_bounds_cache: std::sync::RwLock::new(None),
2853            used_axis_bounds_cache: std::sync::RwLock::new(None),
2854            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
2855            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
2856            source_formula_token: Arc::new(()),
2857            recalc_plan_token: Arc::new(()),
2858            staged_formulas: std::collections::HashMap::new(),
2859            staged_formula_index: StagedFormulaIndex::default(),
2860            blocked_pending_spills: Vec::new(),
2861            row_visibility: FxHashMap::default(),
2862            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
2863            formula_parse_diagnostics: Vec::new(),
2864            last_formula_ingest_report: None,
2865            formula_ingest_report_total: FormulaIngestReport::default(),
2866            active_cancel_flag: None,
2867            active_evaluation_deadline: None,
2868            action_depth: 0,
2869            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
2870            virtual_dep_fallback_activations: 0,
2871            last_cycle_telemetry: CycleTelemetry::default(),
2872            next_evaluation_resource_request_id: 1,
2873            evaluation_resource_request_depth: 0,
2874            active_evaluation_resource_request: None,
2875            last_evaluation_resource_request: None,
2876            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
2877            evaluation_resource_request_started_at: None,
2878            evaluation_resource_budgets: resolved_resources.budgets,
2879            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
2880            active_resource_ledger: None,
2881            source_cache_footprints: Vec::new(),
2882            source_cache_accounted: 0,
2883            pending_iterative_redirty: Vec::new(),
2884            retained_scc_members: FxHashMap::default(),
2885            next_retained_scc_id: 0,
2886            retained_scc_config_fingerprint: 0,
2887            retained_scc_function_epoch_seen: 0,
2888            retained_scc_provider_revision_seen: None,
2889            retained_scc_dirty_at_begin: Vec::new(),
2890            iterative_state_values: FxHashMap::default(),
2891            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
2892            function_provider_revision_seen,
2893            #[cfg(feature = "tracing")]
2894            trace_evaluation_counters: TraceEvaluationCounters::default(),
2895            #[cfg(test)]
2896            evaluation_request_begin_count_for_test: 0,
2897            #[cfg(any(test, feature = "test-support"))]
2898            before_prepared_span_commit_hook: None,
2899            #[cfg(test)]
2900            before_target_preparation_commit_hook: None,
2901            #[cfg(test)]
2902            before_target_planning_snapshot_hook: None,
2903            #[cfg(test)]
2904            inject_target_semantic_stale_once_for_test: false,
2905            #[cfg(test)]
2906            force_virtual_dep_changes_remaining_for_test: 0,
2907            freshness: Default::default(),
2908            #[cfg(test)]
2909            fail_evaluation_commit_preflight_once_for_test: false,
2910            #[cfg(test)]
2911            target_preparation_fault_for_test: None,
2912            #[cfg(test)]
2913            force_non_cycle_schedule_fallback_for_test: false,
2914            #[cfg(test)]
2915            before_legacy_fallback_final_provider_sample_hook: None,
2916            #[cfg(test)]
2917            after_eager_proposal_commit_hook: None,
2918        };
2919        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
2920        engine.config.arrow_storage_enabled = true;
2921        engine.config.delta_overlay_enabled = true;
2922        engine.config.write_formula_overlay_enabled = true;
2923        let default_sheet = engine.graph.default_sheet_name().to_string();
2924        engine.ensure_arrow_sheet(&default_sheet);
2925        engine
2926    }
2927
2928    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
2929        &self.workbook_load_limits
2930    }
2931
2932    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
2933        self.workbook_load_limits = limits;
2934    }
2935
2936    fn clear_source_cache(&self) {
2937        if let Ok(mut g) = self.source_cache.write() {
2938            *g = SourceCache::default();
2939        }
2940    }
2941
2942    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
2943        &self.last_virtual_dep_telemetry
2944    }
2945
2946    /// Telemetry from Runtime SCC evaluation during the most recent
2947    /// evaluation request (always default-zero under `CycleDetection::Static`
2948    /// or when `enable_virtual_dep_telemetry` is off).
2949    pub fn last_cycle_telemetry(&self) -> &CycleTelemetry {
2950        &self.last_cycle_telemetry
2951    }
2952
2953    /// Resource observations for the most recently completed public evaluation request.
2954    pub fn last_evaluation_resource_request_stats(
2955        &self,
2956    ) -> Option<&EvaluationResourceRequestStats> {
2957        self.last_evaluation_resource_request.as_ref()
2958    }
2959
2960    /// Cumulative resource observations since engine creation or the last telemetry reset.
2961    pub fn evaluation_resource_baseline_stats(&self) -> EvaluationResourceBaselineStats {
2962        self.evaluation_resource_baseline
2963    }
2964
2965    pub fn evaluation_resource_budgets(&self) -> &crate::engine::EvaluationBudgets {
2966        &self.evaluation_resource_budgets
2967    }
2968
2969    /// At most one diagnostic is emitted for deprecated resource fields.
2970    pub fn evaluation_resource_config_diagnostic(
2971        &self,
2972    ) -> Option<&crate::engine::EvaluationResourceConfigDiagnostic> {
2973        self.evaluation_resource_config_diagnostic.as_ref()
2974    }
2975
2976    /// Reset accumulated and last-request observations without reusing request IDs.
2977    pub fn reset_evaluation_resource_telemetry(&mut self) {
2978        self.evaluation_resource_baseline = EvaluationResourceBaselineStats::default();
2979        self.last_evaluation_resource_request = None;
2980    }
2981
2982    // Reconcile without replay locks: several packages may share the same Arc.
2983    // Weak tokens neither retain dead packages nor duplicate their allocations.
2984    fn reconcile_source_cache_footprints(&mut self) -> Result<(), ExcelError> {
2985        self.blocked_pending_spills.retain(|&(vertex, anchor, _)| {
2986            self.graph.vertex_exists(vertex)
2987                && self.graph.get_cell_ref(vertex) == Some(anchor)
2988                && matches!(
2989                    self.graph.get_vertex_kind(vertex),
2990                    VertexKind::FormulaScalar | VertexKind::FormulaArray
2991                )
2992        });
2993        if self.blocked_pending_spills.is_empty() {
2994            self.blocked_pending_spills = Vec::new();
2995        }
2996        let mut bytes = (self.blocked_pending_spills.capacity()
2997            * std::mem::size_of::<(VertexId, CellRef, Region)>()) as u64;
2998        self.source_cache_footprints.retain(|weak| {
2999            let Some(footprint) = weak.upgrade() else {
3000                return false;
3001            };
3002            bytes = bytes.saturating_add(footprint.load(std::sync::atomic::Ordering::Acquire));
3003            true
3004        });
3005        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3006            ledger
3007                .release_retained(self.source_cache_accounted)
3008                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3009            // Observe first: even a tightened budget cannot erase live ownership.
3010            ledger.observe_retained(bytes);
3011            self.source_cache_accounted = bytes;
3012            ledger
3013                .reserve_retained(0)
3014                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3015        }
3016        Ok(())
3017    }
3018
3019    fn duration_ns(duration: std::time::Duration) -> u64 {
3020        u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3021    }
3022
3023    fn observe_evaluation_resource_request<T>(
3024        &mut self,
3025        kind: EvaluationRequestKind,
3026        evaluate: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3027    ) -> Result<T, ExcelError> {
3028        let outermost = self.evaluation_resource_request_depth == 0;
3029        #[cfg(feature = "tracing")]
3030        if outermost {
3031            self.trace_evaluation_counters = TraceEvaluationCounters::default();
3032        }
3033        #[cfg(feature = "tracing")]
3034        let request_kind = if matches!(
3035            kind,
3036            EvaluationRequestKind::Full
3037                | EvaluationRequestKind::FullWithDelta
3038                | EvaluationRequestKind::FullCancellable
3039                | EvaluationRequestKind::FullLogged
3040        ) {
3041            "full"
3042        } else {
3043            "targeted"
3044        };
3045        #[cfg(feature = "tracing")]
3046        let _request_span = outermost.then(|| {
3047            crate::engine::trace::fz_span!(
3048                tracing::Level::INFO,
3049                "evaluate",
3050                "evaluate.request",
3051                kind = request_kind,
3052                mode = ?FormulaPlaneMode::Off
3053            )
3054        });
3055        if outermost {
3056            let request_id = self.next_evaluation_resource_request_id;
3057            self.next_evaluation_resource_request_id = request_id
3058                .checked_add(1)
3059                .expect("evaluation resource request ID exhausted");
3060            self.active_evaluation_resource_request = Some(EvaluationResourceRequestStats::new(
3061                request_id,
3062                kind,
3063                FormulaPlaneMode::Off,
3064                self.staged_formula_count(),
3065            ));
3066            self.evaluation_resource_baseline.record_started(request_id);
3067            self.evaluation_resource_request_started_at = Some(crate::instant::FzInstant::now());
3068            self.source_cache_accounted = 0;
3069            self.active_resource_ledger = Some(ResourceLedger::new(
3070                Some(request_id),
3071                self.evaluation_resource_budgets.clone(),
3072            ));
3073        }
3074        self.evaluation_resource_request_depth =
3075            self.evaluation_resource_request_depth.saturating_add(1);
3076        let result = if outermost {
3077            self.reconcile_source_cache_footprints()
3078                .and_then(|()| self.resource_checkpoint(0))
3079                .and_then(|()| evaluate(self))
3080        } else {
3081            evaluate(self)
3082        };
3083        if outermost && result.is_err() {
3084            self.freshness_abort_pass();
3085        }
3086        self.evaluation_resource_request_depth =
3087            self.evaluation_resource_request_depth.saturating_sub(1);
3088
3089        let reconciliation = if outermost {
3090            self.reconcile_source_cache_footprints()
3091        } else {
3092            Ok(())
3093        };
3094        let result = result.and_then(|value| reconciliation.map(|()| value));
3095        if outermost {
3096            let total_ns = self
3097                .evaluation_resource_request_started_at
3098                .take()
3099                .map(|start| Self::duration_ns(start.elapsed()))
3100                .unwrap_or(0);
3101            let mut stats = self
3102                .active_evaluation_resource_request
3103                .take()
3104                .expect("outer evaluation resource request has active stats");
3105            let mut ledger = self
3106                .active_resource_ledger
3107                .take()
3108                .expect("outer evaluation resource request has active ledger");
3109            ledger.release_all_scratch();
3110            stats.ledger.update(ledger.snapshot());
3111            stats.outcome = match &result {
3112                Ok(_) => EvaluationRequestOutcome::Success,
3113                Err(error) if error.kind == ExcelErrorKind::Cancelled => {
3114                    EvaluationRequestOutcome::Cancelled
3115                }
3116                Err(_) => EvaluationRequestOutcome::Error,
3117            };
3118            if stats.dirty_lease == FormulaDirtyLeaseOutcome::Acquired {
3119                stats.dirty_lease = if stats.outcome == EvaluationRequestOutcome::Cancelled {
3120                    FormulaDirtyLeaseOutcome::RetainedOnCancellation
3121                } else {
3122                    FormulaDirtyLeaseOutcome::RetainedOnError
3123                };
3124            }
3125            stats.phases.total_ns = total_ns;
3126            let attributed = stats
3127                .phases
3128                .staged_prepare_ns
3129                .saturating_add(stats.phases.topology_ns)
3130                .saturating_add(stats.phases.materialization_ns);
3131            stats.phases.evaluation_ns = total_ns.saturating_sub(attributed);
3132            self.evaluation_resource_baseline.record_finished(&stats);
3133            self.last_evaluation_resource_request = Some(stats);
3134            crate::engine::trace::fz_event!(
3135                tracing::Level::INFO,
3136                "evaluate",
3137                "evaluate.summary",
3138                computed_vertices = self.trace_evaluation_counters.computed_vertices,
3139                cycles = self.trace_evaluation_counters.cycles,
3140                cancelled = matches!(
3141                    &result,
3142                    Err(error) if error.kind == ExcelErrorKind::Cancelled
3143                )
3144            );
3145        }
3146        result
3147    }
3148
3149    pub fn set_evaluation_resource_budgets(&mut self, budgets: crate::engine::EvaluationBudgets) {
3150        self.evaluation_resource_budgets = budgets.clone();
3151        self.config.evaluation_budgets = budgets.clone();
3152        self.graph.set_evaluation_budgets(budgets);
3153    }
3154
3155    #[cfg(test)]
3156    pub(crate) fn set_evaluation_budgets_for_test(
3157        &mut self,
3158        budgets: crate::engine::EvaluationBudgets,
3159    ) {
3160        self.set_evaluation_resource_budgets(budgets);
3161    }
3162
3163    fn preflight_evaluation_commit_window(
3164        &mut self,
3165        bounded_writes: usize,
3166    ) -> Result<crate::instant::FzInstant, ExcelError> {
3167        #[cfg(test)]
3168        if std::mem::take(&mut self.fail_evaluation_commit_preflight_once_for_test) {
3169            return Err(crate::engine::ResourceLedgerError::Exhausted(
3170                formualizer_common::ResourceExhaustionDetail {
3171                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3172                    limit: 0,
3173                    observed: 1,
3174                    request_id: self
3175                        .active_evaluation_resource_request
3176                        .as_ref()
3177                        .map(|stats| stats.request_id),
3178                },
3179            )
3180            .into_excel_error());
3181        }
3182        let estimate = std::time::Duration::from_nanos(
3183            u64::try_from(bounded_writes)
3184                .unwrap_or(u64::MAX)
3185                .saturating_mul(100),
3186        );
3187        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3188            ledger
3189                .preflight_commit_window(estimate)
3190                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3191        }
3192        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3193            stats.evaluation_commit_preflight_count =
3194                stats.evaluation_commit_preflight_count.saturating_add(1);
3195            stats.evaluation_commit_estimated_ns = stats
3196                .evaluation_commit_estimated_ns
3197                .saturating_add(Self::duration_ns(estimate));
3198        }
3199        Ok(crate::instant::FzInstant::now())
3200    }
3201
3202    fn observe_evaluation_commit_window(&mut self, started: crate::instant::FzInstant) {
3203        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3204            stats.evaluation_commit_actual_ns = stats
3205                .evaluation_commit_actual_ns
3206                .saturating_add(Self::duration_ns(started.elapsed()));
3207        }
3208    }
3209
3210    /// Post-work cancellation boundary: call after evaluating a unit (or a
3211    /// parallel group) and before committing it. A function that observed
3212    /// the request's token returns `Err(Cancelled)`, which the evaluator
3213    /// turns into a `#CANCELLED` value; any other result computed across the
3214    /// signal is equally not a finished result. Neither may publish: the
3215    /// caller returns this error before committing, so the unit stays dirty,
3216    /// the request reports `Cancelled`, and a failed pass restores the
3217    /// vertices it already committed (`freshness_abort_pass`). The token is
3218    /// the discriminator: a `#CANCELLED` value with no live cancellation is
3219    /// ordinary data and commits. Deadlines are not checked here; finished
3220    /// work is not discarded on a deadline.
3221    fn live_cancellation_after_work(&self, message: &'static str) -> Result<(), ExcelError> {
3222        if self
3223            .active_cancel_flag
3224            .as_ref()
3225            .is_some_and(|cancel| cancel.is_cancelled())
3226        {
3227            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3228        }
3229        Ok(())
3230    }
3231
3232    fn cancellation_checkpoint(&self, message: &'static str) -> Result<(), ExcelError> {
3233        if self
3234            .active_cancel_flag
3235            .as_ref()
3236            .is_some_and(|cancel| cancel.is_cancelled())
3237        {
3238            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3239        }
3240        if self
3241            .active_evaluation_deadline
3242            .is_some_and(|deadline| Instant::now() >= deadline)
3243        {
3244            return Err(crate::engine::ResourceLedgerError::Exhausted(
3245                formualizer_common::ResourceExhaustionDetail {
3246                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3247                    limit: 0,
3248                    observed: 1,
3249                    request_id: self
3250                        .active_evaluation_resource_request
3251                        .as_ref()
3252                        .map(|request| request.request_id),
3253                },
3254            )
3255            .into_excel_error()
3256            .with_message(message));
3257        }
3258        Ok(())
3259    }
3260
3261    fn resource_checkpoint(&mut self, work_units: u64) -> Result<(), ExcelError> {
3262        let Some(ledger) = self.active_resource_ledger.as_mut() else {
3263            return Ok(());
3264        };
3265        ledger
3266            .charge_work(work_units)
3267            .and_then(|()| ledger.checkpoint_deadline())
3268            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3269    }
3270
3271    fn charge_bounded_work(&mut self, mut work_units: u64) -> Result<(), ExcelError> {
3272        if work_units == 0 {
3273            return self.resource_checkpoint(0);
3274        }
3275        while work_units > 0 {
3276            let chunk = work_units.min(256);
3277            self.resource_checkpoint(chunk)?;
3278            work_units -= chunk;
3279        }
3280        Ok(())
3281    }
3282
3283    fn reserve_request_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3284        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3285            // Exact request topology and activation of the scratch cap are C1b. C1a records
3286            // scoped ownership but must not introduce a new skip or terminal path.
3287            ledger.observe_scratch(bytes);
3288        }
3289        Ok(())
3290    }
3291
3292    fn release_request_scratch(&mut self, bytes: u64) {
3293        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3294            let released = ledger.release_scratch(bytes);
3295            debug_assert!(
3296                released.is_ok(),
3297                "request scratch release exceeded the outstanding reservation"
3298            );
3299        }
3300    }
3301
3302    fn reserve_topology_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3303        self.active_resource_ledger
3304            .as_mut()
3305            .map_or(Ok(()), |ledger| ledger.reserve_schedule_discovery(bytes))
3306            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3307    }
3308
3309    fn reserve_graph_source_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3310        self.active_resource_ledger
3311            .as_mut()
3312            .map_or(Ok(()), |ledger| ledger.reserve_graph_source(bytes))
3313            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3314    }
3315
3316    fn with_request_scratch<T>(
3317        &mut self,
3318        bytes: u64,
3319        work: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3320    ) -> Result<T, ExcelError> {
3321        self.reserve_request_scratch(bytes)?;
3322        let result = work(self);
3323        self.release_request_scratch(bytes);
3324        result
3325    }
3326
3327    fn observe_staged_preparation(
3328        &mut self,
3329        selected: usize,
3330        retained: usize,
3331        elapsed: std::time::Duration,
3332    ) {
3333        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3334            stats.staged_selected = stats.staged_selected.saturating_add(selected as u64);
3335            stats.staged_retained = retained as u64;
3336            stats.phases.staged_prepare_ns = stats
3337                .phases
3338                .staged_prepare_ns
3339                .saturating_add(Self::duration_ns(elapsed));
3340        }
3341    }
3342
3343    fn graph_admission_enabled(&self) -> bool {
3344        crate::engine::resource_ledger::graph_admission_enabled(&self.evaluation_resource_budgets)
3345    }
3346
3347    fn preflight_graph_admission(
3348        &mut self,
3349        usage: crate::engine::resource_ledger::GraphAdmission,
3350    ) -> Result<(), ExcelError> {
3351        let request_id = self
3352            .active_evaluation_resource_request
3353            .as_ref()
3354            .map(|stats| stats.request_id);
3355        crate::engine::resource_ledger::preflight_graph_admission(
3356            &self.evaluation_resource_budgets,
3357            usage,
3358            request_id,
3359        )
3360        .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3361    }
3362
3363    fn prepared_legacy_admission(
3364        &mut self,
3365        plan: &PreparedLegacyGraphPlan,
3366        materialization_cells: u64,
3367    ) -> Result<(), ExcelError> {
3368        if !self.graph_admission_enabled() {
3369            return Ok(());
3370        }
3371        let stats = self.graph.baseline_stats();
3372        let added_edges = plan.planned_edge_count().ok_or_else(|| {
3373            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3374        })?;
3375        let removed_edges = plan.removed_edge_count().ok_or_else(|| {
3376            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3377        })?;
3378        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
3379            final_vertices: stats
3380                .graph_vertex_count
3381                .checked_add(plan.new_vertex_count())
3382                .ok_or_else(|| {
3383                    ExcelError::new(ExcelErrorKind::NImpl)
3384                        .with_message("graph vertex count overflow")
3385                })?,
3386            final_edges: stats
3387                .graph_edge_count
3388                .checked_sub(removed_edges)
3389                .and_then(|count| count.checked_add(added_edges))
3390                .ok_or_else(|| {
3391                    ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3392                })?,
3393            materialization_cells,
3394            added_vertices: plan.new_vertex_count(),
3395            added_edges,
3396        })
3397    }
3398
3399    fn observe_target_admission_failure(
3400        &mut self,
3401        reason: formualizer_common::ResourceExhaustionReason,
3402    ) {
3403        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3404            stats.target_admission_failure = Some(reason);
3405        }
3406    }
3407
3408    fn observe_target_preparation_report(
3409        &mut self,
3410        report: &crate::engine::PreparedTargetGraphReport,
3411    ) {
3412        let reason_bit = |reason: crate::engine::OpaqueReason| -> u64 {
3413            let index = match reason {
3414                crate::engine::OpaqueReason::DynamicReference => 0,
3415                crate::engine::OpaqueReason::RuntimeTextReference => 1,
3416                crate::engine::OpaqueReason::UnknownFunction => 2,
3417                crate::engine::OpaqueReason::UnknownCustomFunction => 3,
3418                crate::engine::OpaqueReason::UnresolvedCrossSheetBinding => 4,
3419                crate::engine::OpaqueReason::UnresolvedName => 5,
3420                crate::engine::OpaqueReason::UnresolvedTable => 6,
3421                crate::engine::OpaqueReason::FormulaName => 7,
3422                crate::engine::OpaqueReason::DeferredSourcePackage => 8,
3423                crate::engine::OpaqueReason::UnsupportedSourceSemantics => 9,
3424                crate::engine::OpaqueReason::UncertainDefaultSheetBinding => 10,
3425            };
3426            1u64 << index
3427        };
3428        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3429            stats.staged_selected = report.selected_staged_cells as u64;
3430            stats.staged_retained = report.retained_staged_cells as u64;
3431            stats.target_requested = report.requested_targets as u64;
3432            stats.target_normalized_regions = report.normalized_regions as u64;
3433            stats.target_scope_level = match &report.widened_scope {
3434                crate::engine::PrepareScope::Exact => 0,
3435                crate::engine::PrepareScope::Sheets(_) => 1,
3436                crate::engine::PrepareScope::Workbook => 2,
3437            };
3438            stats.target_widening_reason_bits = report
3439                .widening_reasons
3440                .iter()
3441                .copied()
3442                .fold(0, |bits, reason| bits | reason_bit(reason));
3443            stats.graph_source_scratch_estimated = report.estimated_scratch_bytes;
3444            stats.graph_source_scratch_observed = report.observed_scratch_bytes;
3445            stats.target_commit_estimated_work = report.estimated_commit_work;
3446            stats.target_commit_actual_work = report.actual_commit_work;
3447            stats.target_commit_window_ns = Self::duration_ns(report.commit_window);
3448            stats.phases.staged_prepare_ns = stats
3449                .phases
3450                .staged_prepare_ns
3451                .saturating_add(Self::duration_ns(report.commit_window));
3452        }
3453    }
3454
3455    /// Begin a new evaluation request: reset per-recalc cycle telemetry and
3456    /// take the per-recalc volatile clock sample. Called at the start of
3457    /// every evaluation request that walks schedule units.
3458    fn begin_evaluation_request(&mut self) {
3459        self.freshness_begin_request();
3460        #[cfg(test)]
3461        {
3462            self.evaluation_request_begin_count_for_test = self
3463                .evaluation_request_begin_count_for_test
3464                .saturating_add(1);
3465        }
3466        self.last_cycle_telemetry = CycleTelemetry::default();
3467        self.graph.authority_sync();
3468        // Defensive: consumed at the end of the previous request; a request
3469        // that errored out mid-walk must not leak its members into this one.
3470        self.pending_iterative_redirty.clear();
3471        self.reconcile_retained_sccs_at_request_begin();
3472        // Spec §7.11: NOW()/TODAY() sample the clock ONCE per recalc; every
3473        // read within this request (including SCC iteration passes) observes
3474        // this sample.
3475        self.clock.refresh();
3476    }
3477
3478    /// End-of-recalc redirty: volatile vertices (as always) plus members of
3479    /// SCCs that iterated this recalc without reaching a retainable fixed
3480    /// point (`CyclePolicy::Iterate`), so circular cells re-evaluate on every
3481    /// recalc exactly like Excel's iterative calculation (spec §4
3482    /// persistence / §7.6 accumulator / §7.11 volatile redirty). Retained
3483    /// SCCs (`retained_scc_members`, #368) are left clean. Replaces the bare
3484    /// `graph.redirty_volatiles()` call at every evaluation-flow exit; must
3485    /// run AFTER the flow's `clear_dirty_flags`.
3486    fn redirty_for_next_recalc(&mut self) {
3487        self.graph.redirty_volatiles();
3488        let pending = std::mem::take(&mut self.pending_iterative_redirty);
3489        let dirty_at_begin = std::mem::take(&mut self.retained_scc_dirty_at_begin);
3490        for (vertex, scc) in dirty_at_begin {
3491            if self.retained_scc_members.get(&vertex) == Some(&scc) {
3492                // No SCC task claimed this member during the request: the
3493                // cycle dissolved and it evaluated as an ordinary formula,
3494                // or the request never reached it. Either way it is no
3495                // longer a retained fixed point. Its persisted value is
3496                // obsolete only if it actually re-evaluated (clean now).
3497                self.retained_scc_members.remove(&vertex);
3498                if !self.graph.is_dirty(vertex) {
3499                    self.iterative_state_values.remove(&vertex);
3500                }
3501            }
3502        }
3503        if !self.iterative_state_values.is_empty() || !self.retained_scc_members.is_empty() {
3504            let graph = &self.graph;
3505            self.iterative_state_values
3506                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3507            self.retained_scc_members
3508                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3509        }
3510        // Refresh the §4-persistence snapshot for members that re-run each
3511        // recalc: these final values survive structural edits that clear the
3512        // computed overlay (the only value home in canonical mode) so the
3513        // next SCC task can re-seed from them (see `iterative_state_values`).
3514        for &vertex in &pending {
3515            if !self.graph.is_live_formula_vertex(vertex) {
3516                continue;
3517            }
3518            if let Some(cell) = self.graph.get_cell_ref(vertex) {
3519                let sheet_name = self.graph.sheet_name(cell.sheet_id);
3520                match self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1) {
3521                    Some(value) if !matches!(value, LiteralValue::Empty) => {
3522                        self.iterative_state_values.insert(vertex, value);
3523                    }
3524                    _ => {
3525                        self.iterative_state_values.remove(&vertex);
3526                    }
3527                }
3528            }
3529        }
3530        if !pending.is_empty() {
3531            self.graph.redirty_iterative_members(&pending);
3532        }
3533    }
3534
3535    /// Hash of every `EvalConfig` knob that can change the result of a
3536    /// retained SCC without any edit reaching the dependency graph. The
3537    /// function registry is tracked separately and precisely (see
3538    /// `retained_scc_function_epoch_seen`).
3539    fn retained_scc_config_fingerprint(&self) -> u64 {
3540        use std::hash::{Hash, Hasher};
3541        let mut hasher = rustc_hash::FxHasher::default();
3542        let config = &self.config;
3543        std::mem::discriminant(&config.cycle.detection).hash(&mut hasher);
3544        match config.cycle.policy {
3545            CyclePolicy::Error => 0u8.hash(&mut hasher),
3546            CyclePolicy::Iterate {
3547                max_iterations,
3548                max_change,
3549            } => {
3550                1u8.hash(&mut hasher);
3551                max_iterations.hash(&mut hasher);
3552                max_change.to_bits().hash(&mut hasher);
3553            }
3554        }
3555        config.date_system.hash(&mut hasher);
3556        config.workbook_seed.hash(&mut hasher);
3557        std::mem::discriminant(&config.volatile_level).hash(&mut hasher);
3558        // `DeterministicMode` carries a timestamp and a timezone spec; hash
3559        // its Debug rendering rather than growing its derive set for this.
3560        format!("{:?}", config.deterministic_mode).hash(&mut hasher);
3561        config.range_expansion_limit.hash(&mut hasher);
3562        config.max_open_ended_rows.hash(&mut hasher);
3563        config.max_open_ended_cols.hash(&mut hasher);
3564        hasher.finish()
3565    }
3566
3567    /// Request-begin bookkeeping for retained SCCs (#368): drop deleted
3568    /// vertices, invalidate everything when the config fingerprint moved
3569    /// (marking retained members dirty so their SCC tasks run in this
3570    /// request), and record how many retained SCCs are being reused, i.e.
3571    /// have no dirty member at request begin.
3572    fn reconcile_retained_sccs_at_request_begin(&mut self) {
3573        self.retained_scc_dirty_at_begin.clear();
3574        if self.retained_scc_members.is_empty() {
3575            return;
3576        }
3577        // Deleted vertices and members overwritten with a literal (the
3578        // vertex survives as a value cell) are no longer retained formulas.
3579        let graph = &self.graph;
3580        self.retained_scc_members
3581            .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3582        if self.retained_scc_config_fingerprint() != self.retained_scc_config_fingerprint {
3583            let members: Vec<VertexId> = self.retained_scc_members.keys().copied().collect();
3584            self.retained_scc_members.clear();
3585            self.graph.mark_dirty_many(&members);
3586            return;
3587        }
3588        let changes =
3589            crate::function_registry::semantic_changes_since(self.retained_scc_function_epoch_seen);
3590        let global_changed = changes.epoch != self.retained_scc_function_epoch_seen;
3591        let provider_revision = self.resolver.planning_semantic_revision();
3592        let provider_changed = provider_revision != self.retained_scc_provider_revision_seen;
3593        if global_changed || provider_changed {
3594            let changed: BTreeSet<(String, String)> = changes.keys.into_iter().collect();
3595            let affected: Vec<VertexId> = self
3596                .retained_scc_members
3597                .keys()
3598                .copied()
3599                .filter(|&vertex| {
3600                    let Some(ast) = self.graph.get_formula(vertex) else {
3601                        return true;
3602                    };
3603                    (global_changed
3604                        && (!changes.complete || Self::ast_uses_changed_function(&ast, &changed)))
3605                        || (provider_changed && Self::ast_contains_function(&ast))
3606                })
3607                .collect();
3608            for vertex in &affected {
3609                self.retained_scc_members.remove(vertex);
3610            }
3611            if !affected.is_empty() {
3612                self.graph.mark_dirty_many(&affected);
3613            }
3614            self.retained_scc_function_epoch_seen = changes.epoch;
3615            self.retained_scc_provider_revision_seen = provider_revision;
3616        }
3617        let mut dirty_sccs: FxHashSet<u64> = FxHashSet::default();
3618        let mut all_sccs: FxHashSet<u64> = FxHashSet::default();
3619        for (&vertex, &scc) in &self.retained_scc_members {
3620            all_sccs.insert(scc);
3621            if self.graph.is_dirty(vertex) {
3622                dirty_sccs.insert(scc);
3623                self.retained_scc_dirty_at_begin.push((vertex, scc));
3624            }
3625        }
3626        let reused_members = self
3627            .retained_scc_members
3628            .values()
3629            .filter(|scc| !dirty_sccs.contains(scc))
3630            .count();
3631        let t = &mut self.last_cycle_telemetry;
3632        t.reused_sccs = all_sccs.len() - dirty_sccs.len();
3633        t.reused_scc_members = reused_members;
3634    }
3635
3636    pub fn virtual_dep_fallback_activations(&self) -> u64 {
3637        self.virtual_dep_fallback_activations
3638    }
3639
3640    #[cfg(test)]
3641    pub(crate) fn lookup_index_flights_built_for_test(&self) -> usize {
3642        self.lookup_index_cache
3643            .flights_built
3644            .load(std::sync::atomic::Ordering::Relaxed)
3645    }
3646
3647    pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
3648        self.lookup_index_cache.report()
3649    }
3650
3651    fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
3652        let start_row = view.start_row();
3653        let end_row = view.end_row();
3654        let start_col = view.start_col();
3655        let end_col = view.end_col();
3656        for row in start_row..=end_row {
3657            let Ok(row_u32) = u32::try_from(row) else {
3658                return true;
3659            };
3660            for col in start_col..=end_col {
3661                let Ok(col_u32) = u32::try_from(col) else {
3662                    return true;
3663                };
3664                let cell_ref = self
3665                    .graph
3666                    .make_cell_ref_internal(sheet_id, row_u32, col_u32);
3667                if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
3668                    && self.graph.is_volatile(vertex_id)
3669                {
3670                    return true;
3671                }
3672            }
3673        }
3674        false
3675    }
3676
3677    fn build_lookup_index_impl(
3678        &self,
3679        view: &RangeView<'_>,
3680        axis: LookupAxis,
3681    ) -> Option<Arc<LookupIndex>> {
3682        let (rows, cols) = view.dims();
3683        if rows == 0 || cols == 0 {
3684            self.lookup_index_cache.note_skipped_tiny();
3685            return None;
3686        }
3687        let len = match axis {
3688            LookupAxis::ColumnInView(col) => {
3689                if col >= cols {
3690                    self.lookup_index_cache.note_skipped_tiny();
3691                    return None;
3692                }
3693                rows
3694            }
3695            LookupAxis::RowInView(row) => {
3696                if row >= rows {
3697                    self.lookup_index_cache.note_skipped_tiny();
3698                    return None;
3699                }
3700                cols
3701            }
3702        };
3703        if len < 64 {
3704            self.lookup_index_cache.note_skipped_tiny();
3705            return None;
3706        }
3707
3708        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
3709        let key = LookupIndexKey {
3710            sheet_id,
3711            start_row: u32::try_from(view.start_row()).ok()?,
3712            start_col: u32::try_from(view.start_col()).ok()?,
3713            end_row: u32::try_from(view.end_row()).ok()?,
3714            end_col: u32::try_from(view.end_col()).ok()?,
3715            axis,
3716            snapshot_id: self.data_snapshot_id(),
3717        };
3718        if let Some(index) = self.lookup_index_cache.get(&key) {
3719            return Some(index);
3720        }
3721        if self
3722            .lookup_index_cache
3723            .would_exceed_cap(estimate_bytes(len, 0))
3724        {
3725            self.lookup_index_cache.note_skipped_cap();
3726            return None;
3727        }
3728        if !self.lookup_index_cache.should_build(key) {
3729            return None;
3730        }
3731        // Parallel members of a lookup family miss together: one builds.
3732        self.lookup_index_cache.single_flight(key, || {
3733            if let Some(index) = self.lookup_index_cache.recheck(&key) {
3734                return Some(index);
3735            }
3736            if self.lookup_index_cache.is_known_volatile(&key) {
3737                self.lookup_index_cache.note_skipped_volatile();
3738                return None;
3739            }
3740            if self.lookup_view_contains_volatile(view, sheet_id) {
3741                self.lookup_index_cache.note_volatile_key(key);
3742                self.lookup_index_cache.note_skipped_volatile();
3743                return None;
3744            }
3745            #[cfg(test)]
3746            self.lookup_index_cache
3747                .flights_built
3748                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3749            match LookupIndex::build(view, axis, self.config.date_system).ok()? {
3750                BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
3751                BuildOutcome::ErrorInLookupAxis => {
3752                    self.lookup_index_cache.note_skipped_error();
3753                    None
3754                }
3755                BuildOutcome::Degenerate => {
3756                    self.lookup_index_cache.note_skipped_tiny();
3757                    None
3758                }
3759            }
3760        })
3761    }
3762
3763    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
3764        if !self.config.enable_virtual_dep_telemetry {
3765            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
3766                fallback_mode_activations: self.virtual_dep_fallback_activations,
3767                ..VirtualDepTelemetry::default()
3768            };
3769        }
3770    }
3771
3772    fn source_cache_session(&self) -> SourceCacheSession {
3773        self.clear_source_cache();
3774        SourceCacheSession {
3775            cache: self.source_cache.clone(),
3776        }
3777    }
3778
3779    fn resolve_source_scalar_cached(
3780        &self,
3781        name: &str,
3782        version: Option<u64>,
3783    ) -> Result<LiteralValue, ExcelError> {
3784        let key = (name.to_string(), version);
3785        if let Ok(mut g) = self.source_cache.write() {
3786            if let Some(v) = g.scalars.get(&key) {
3787                return Ok(v.clone());
3788            }
3789
3790            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
3791                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3792                    ExcelError::new(ExcelErrorKind::Ref)
3793                        .with_message(format!("Unresolved source scalar: {name}"))
3794                } else {
3795                    err
3796                }
3797            })?;
3798            g.scalars.insert(key, v.clone());
3799            Ok(v)
3800        } else {
3801            self.resolver.resolve_source_scalar(name).map_err(|err| {
3802                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3803                    ExcelError::new(ExcelErrorKind::Ref)
3804                        .with_message(format!("Unresolved source scalar: {name}"))
3805                } else {
3806                    err
3807                }
3808            })
3809        }
3810    }
3811
3812    fn resolve_source_table_cached(
3813        &self,
3814        name: &str,
3815        version: Option<u64>,
3816    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
3817        let key = (name.to_string(), version);
3818        if let Ok(mut g) = self.source_cache.write() {
3819            if let Some(t) = g.tables.get(&key) {
3820                return Ok(t.clone());
3821            }
3822
3823            let t = self.resolver.resolve_source_table(name).map_err(|err| {
3824                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3825                    ExcelError::new(ExcelErrorKind::Ref)
3826                        .with_message(format!("Unresolved source table: {name}"))
3827                } else {
3828                    err
3829                }
3830            })?;
3831            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
3832            g.tables.insert(key, t.clone());
3833            Ok(t)
3834        } else {
3835            self.resolver
3836                .resolve_source_table(name)
3837                .map_err(|err| {
3838                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
3839                        ExcelError::new(ExcelErrorKind::Ref)
3840                            .with_message(format!("Unresolved source table: {name}"))
3841                    } else {
3842                        err
3843                    }
3844                })
3845                .map(Arc::from)
3846        }
3847    }
3848
3849    fn source_table_to_range_view(
3850        &self,
3851        table: &dyn crate::traits::Table,
3852        spec: &Option<formualizer_parse::parser::TableSpecifier>,
3853    ) -> Result<RangeView<'static>, ExcelError> {
3854        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
3855
3856        let owned = match spec {
3857            Some(TableSpecifier::Column(c)) => {
3858                let c = c.trim();
3859                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
3860                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
3861                        "Complex structured references not yet supported".to_string(),
3862                    ));
3863                }
3864                table.get_column(c)?.materialise().into_owned()
3865            }
3866            Some(TableSpecifier::ColumnRange(start, end)) => {
3867                let cols = table.columns();
3868                let start = start.trim();
3869                let end = end.trim();
3870                let start_key = start.to_lowercase();
3871                let end_key = end.to_lowercase();
3872                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
3873                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
3874                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
3875                    if si > ei {
3876                        std::mem::swap(&mut si, &mut ei);
3877                    }
3878                    let h = table.data_height();
3879                    let w = ei - si + 1;
3880                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
3881                    for (offset, ci) in (si..=ei).enumerate() {
3882                        let cname = &cols[ci];
3883                        let col_range = table.get_column(cname)?;
3884                        let (rh, _) = col_range.dimensions();
3885                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
3886                            row[offset] = col_range.get(r, 0)?;
3887                        }
3888                    }
3889                    rows
3890                } else {
3891                    return Err(ExcelError::new(ExcelErrorKind::Ref)
3892                        .with_message("Column range refers to unknown column(s)".to_string()));
3893                }
3894            }
3895            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
3896            | Some(TableSpecifier::Headers) => table
3897                .headers_row()
3898                .map(|r| r.materialise().into_owned())
3899                .unwrap_or_default(),
3900            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
3901            | Some(TableSpecifier::Totals) => table
3902                .totals_row()
3903                .map(|r| r.materialise().into_owned())
3904                .unwrap_or_default(),
3905            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
3906                table
3907                    .data_body()
3908                    .map(|r| r.materialise().into_owned())
3909                    .unwrap_or_default()
3910            }
3911            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
3912                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
3913                if let Some(h) = table.headers_row() {
3914                    out.extend(h.iter_rows());
3915                }
3916                if let Some(body) = table.data_body() {
3917                    out.extend(body.iter_rows());
3918                }
3919                if let Some(tr) = table.totals_row() {
3920                    out.extend(tr.iter_rows());
3921                }
3922                out
3923            }
3924            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
3925                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
3926                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
3927                ));
3928            }
3929            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
3930                return Err(ExcelError::new(ExcelErrorKind::NImpl)
3931                    .with_message("Complex structured references not yet supported".to_string()));
3932            }
3933            None => {
3934                return Err(ExcelError::new(ExcelErrorKind::NImpl)
3935                    .with_message("Table reference without specifier is unsupported".to_string()));
3936            }
3937        };
3938
3939        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
3940    }
3941
3942    pub fn default_sheet_id(&self) -> SheetId {
3943        self.graph.default_sheet_id()
3944    }
3945
3946    pub fn default_sheet_name(&self) -> &str {
3947        self.graph.default_sheet_name()
3948    }
3949
3950    /// Update the workbook seed for deterministic RNGs in functions.
3951    pub fn set_workbook_seed(&mut self, seed: u64) {
3952        self.config.workbook_seed = seed;
3953    }
3954
3955    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
3956    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
3957        self.config.volatile_level = level;
3958    }
3959
3960    /// Set public temporal materialisation to native values or raw serials.
3961    pub fn set_temporal_egress(&mut self, policy: crate::engine::TemporalEgress) {
3962        self.config.temporal_egress = policy;
3963    }
3964
3965    pub fn temporal_egress(&self) -> crate::engine::TemporalEgress {
3966        self.config.temporal_egress
3967    }
3968
3969    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
3970    pub fn set_deterministic_mode(
3971        &mut self,
3972        mode: crate::engine::DeterministicMode,
3973    ) -> Result<(), ExcelError> {
3974        let clock = mode.build_clock()?;
3975        self.config.deterministic_mode = mode;
3976        self.clock = crate::timezone::SnapshotClock::new(clock);
3977        Ok(())
3978    }
3979
3980    /// Inject a custom [`ClockProvider`](crate::timezone::ClockProvider) for
3981    /// volatile date/time builtins (`NOW()`, `TODAY()`).
3982    ///
3983    /// The provider is the clock *source*; per spec §7.11 the engine samples
3984    /// it once at the start of every evaluation request and all reads within
3985    /// that recalc (including SCC iteration passes) observe the frozen
3986    /// sample.
3987    pub fn set_clock(&mut self, clock: Arc<dyn crate::timezone::ClockProvider>) {
3988        self.clock = crate::timezone::SnapshotClock::new(clock);
3989    }
3990
3991    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
3992        self.config.deterministic_mode.validate()
3993    }
3994
3995    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
3996        self.graph.sheet_id(name)
3997    }
3998
3999    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
4000        self.add_sheet(name)
4001            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
4002    }
4003
4004    pub fn sheet_name(&self, id: SheetId) -> &str {
4005        self.graph.sheet_name(id)
4006    }
4007
4008    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
4009        let id = self.graph.add_sheet(name)?;
4010        self.ensure_arrow_sheet(name);
4011        self.mark_topology_edited();
4012        Ok(id)
4013    }
4014
4015    pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
4016        let source_id = self.graph.sheet_id(source).ok_or_else(|| {
4017            ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
4018        })?;
4019        if new_name.is_empty() || new_name.len() > 255 {
4020            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
4021        }
4022        if self.graph.sheet_id(new_name).is_some() {
4023            return Err(ExcelError::new(ExcelErrorKind::Value)
4024                .with_message(format!("Sheet '{new_name}' already exists")));
4025        }
4026        let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
4027
4028        if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
4029            let mut copied_sheet = source_sheet;
4030            copied_sheet.name = Arc::<str>::from(new_name);
4031            self.arrow_sheets.sheets.push(copied_sheet);
4032        } else {
4033            self.ensure_arrow_sheet(new_name);
4034        }
4035
4036        let duplicated_formulas = self
4037            .graph
4038            .formula_vertices()
4039            .into_iter()
4040            .filter(|vertex| {
4041                self.graph
4042                    .get_cell_ref(*vertex)
4043                    .is_some_and(|cell| cell.sheet_id == new_id)
4044            })
4045            .collect::<Vec<_>>();
4046        self.graph.mark_vertices_dirty_batch(&duplicated_formulas);
4047        self.mark_topology_edited();
4048        Ok(new_id)
4049    }
4050
4051    fn ensure_arrow_sheet(&mut self, name: &str) {
4052        if self.arrow_sheets.sheet(name).is_some() {
4053            return;
4054        }
4055        self.arrow_sheets
4056            .sheets
4057            .push(crate::arrow_store::ArrowSheet {
4058                name: std::sync::Arc::<str>::from(name),
4059                date_system: self.config.date_system,
4060                columns: Vec::new(),
4061                nrows: 0,
4062                chunk_starts: Vec::new(),
4063                chunk_rows: 32 * 1024,
4064            });
4065    }
4066
4067    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
4068        let name = self.graph.sheet_name(sheet_id).to_string();
4069        self.purge_derived_formats_for_sheet(sheet_id);
4070        self.graph.remove_sheet(sheet_id)?;
4071        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
4072        // Sheet removal can change cross-sheet refs, names, and default-sheet
4073        // resolution. Until those domains have a complete exact dependency
4074        // proof, retain the documented graph-owned global invalidation.
4075        self.clear_all_computed_overlays();
4076        self.mark_all_formula_vertices_dirty();
4077        self.clear_staged_formulas_for_sheet(&name);
4078        if self.row_visibility.remove(&sheet_id).is_some() {
4079            self.invalidate_row_visibility_mask_cache();
4080        }
4081        self.record_structural_change(StructuralScope::RemovedSheet(sheet_id));
4082        self.mark_topology_edited();
4083        Ok(())
4084    }
4085
4086    /// Helper to synchronize the Arrow-backed storage layer.
4087    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
4088        if let Some(asheet) = self
4089            .arrow_sheets
4090            .sheets
4091            .iter_mut()
4092            .find(|s| s.name.as_ref() == target_name)
4093        {
4094            asheet.name = std::sync::Arc::<str>::from(new_name);
4095            return true;
4096        }
4097        false
4098    }
4099
4100    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
4101        let old_name = self.graph.sheet_name(sheet_id).to_string();
4102
4103        // Speculative Storage Update
4104        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
4105        self.rename_sheet_in_arrow_store(&old_name, new_name);
4106
4107        // Graph Update (Metadata + Rescue Logic)
4108        match self.graph.rename_sheet(sheet_id, new_name) {
4109            Ok(_) => {
4110                self.rename_staged_formula_sheet(&old_name, new_name);
4111                // Success! Invalidate cache for the moved sheet
4112                let sheet_vertices: Vec<VertexId> = self
4113                    .graph
4114                    .grid_vertices_in_sheet(sheet_id)
4115                    .map(|(id, _)| id)
4116                    .collect();
4117                for v_id in sheet_vertices {
4118                    self.graph.mark_vertex_dirty(v_id);
4119                }
4120                // Sheet rename preserves SheetId and therefore formula dependencies.
4121                self.mark_topology_edited();
4122                Ok(())
4123            }
4124            Err(e) => {
4125                // ROLLBACK: Revert storage if graph rejected the name
4126                self.rename_sheet_in_arrow_store(new_name, &old_name);
4127                Err(e)
4128            }
4129        }
4130    }
4131
4132    pub fn named_ranges_iter(
4133        &self,
4134    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
4135        self.graph.named_ranges_iter()
4136    }
4137
4138    pub fn sheet_named_ranges_iter(
4139        &self,
4140    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
4141        self.graph.sheet_named_ranges_iter()
4142    }
4143
4144    pub fn resolve_name_entry(
4145        &self,
4146        name: &str,
4147        current_sheet: SheetId,
4148    ) -> Option<&crate::engine::named_range::NamedRange> {
4149        self.graph.resolve_name_entry(name, current_sheet)
4150    }
4151
4152    /// The [`NameScope`] an optional scope-sheet argument denotes.
4153    ///
4154    /// `None` means **workbook scope**, not "the default sheet": a caller that
4155    /// supplies no sheet context is asking about workbook-scoped names only.
4156    /// An unknown sheet name is a malformed query and errors rather than
4157    /// silently degrading to another sheet's scope (issue #110).
4158    ///
4159    /// This is the one owned derivation from `Option<&str>` to a name scope;
4160    /// every scope-taking entry point routes through it.
4161    pub(crate) fn name_query_scope(
4162        &self,
4163        scope_sheet: Option<&str>,
4164    ) -> Result<NameScope, ExcelError> {
4165        match scope_sheet {
4166            None => Ok(NameScope::Workbook),
4167            Some(sheet) => self
4168                .graph
4169                .sheet_id(sheet)
4170                .map(NameScope::Sheet)
4171                .ok_or_else(|| {
4172                    ExcelError::new(ExcelErrorKind::Ref)
4173                        .with_message(format!("name scope sheet not found: {sheet}"))
4174                }),
4175        }
4176    }
4177
4178    /// Whether `name` resolves in the scope denoted by `scope_sheet`.
4179    ///
4180    /// `scope_sheet == None` asks about workbook scope only; a name scoped to a
4181    /// single sheet (including the default sheet) does not answer it. An unknown
4182    /// sheet name resolves nothing.
4183    /// Resolve a [`SharedSheetLocator`](crate::reference::SharedSheetLocator)
4184    /// against an explicit context sheet.
4185    ///
4186    /// Thin forwarder to
4187    /// [`SheetRegistry::resolve_locator`](crate::engine::sheet_registry::SheetRegistry::resolve_locator),
4188    /// the single owned derivation. `Current` resolves to `context_sheet`, never
4189    /// to the workbook's default sheet.
4190    fn resolve_sheet_locator(
4191        &self,
4192        locator: &crate::reference::SharedSheetLocator<'_>,
4193        context_sheet: SheetId,
4194    ) -> Result<SheetId, ExcelError> {
4195        self.graph
4196            .sheet_reg()
4197            .resolve_locator(locator, context_sheet)
4198    }
4199
4200    pub fn has_name(&self, name: &str, scope_sheet: Option<&str>) -> bool {
4201        let Ok(scope) = self.name_query_scope(scope_sheet) else {
4202            return false;
4203        };
4204        self.graph
4205            .resolve_name_entry_in_scope(name, scope)
4206            .is_some()
4207    }
4208
4209    /// The current value of `name` in the scope denoted by `scope_sheet`.
4210    ///
4211    /// Scoping follows [`Self::has_name`]: `None` is workbook scope only.
4212    pub fn resolved_name_value(
4213        &self,
4214        name: &str,
4215        scope_sheet: Option<&str>,
4216    ) -> Option<LiteralValue> {
4217        let scope = self.name_query_scope(scope_sheet).ok()?;
4218        let entry = self.graph.resolve_name_entry_in_scope(name, scope)?;
4219        self.graph.get_value(entry.vertex)
4220    }
4221
4222    pub fn table_metadata(&self, name: &str) -> Option<TableMetadata> {
4223        let entry = self.graph.resolve_table_entry(name)?;
4224        Some(TableMetadata {
4225            name: entry.name.clone(),
4226            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
4227            start_row: entry.range.start.coord.row() + 1,
4228            start_col: entry.range.start.coord.col() + 1,
4229            end_row: entry.range.end.coord.row() + 1,
4230            end_col: entry.range.end.coord.col() + 1,
4231            header_row: entry.header_row,
4232            headers: entry.headers.clone(),
4233            totals_row: entry.totals_row,
4234        })
4235    }
4236
4237    /// Metadata for every defined table, ordered by name.
4238    pub fn tables(&self) -> Vec<TableMetadata> {
4239        self.graph
4240            .table_names()
4241            .into_iter()
4242            .filter_map(|name| self.table_metadata(&name))
4243            .collect()
4244    }
4245
4246    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4247        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
4248
4249        for (name, named) in self.graph.named_ranges_iter() {
4250            out.push(crate::engine::named_range::NamedRangeSnapshot {
4251                name: name.clone(),
4252                scope: NameScope::Workbook,
4253                definition: named.definition.clone(),
4254            });
4255        }
4256
4257        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
4258            out.push(crate::engine::named_range::NamedRangeSnapshot {
4259                name: name.clone(),
4260                scope: NameScope::Sheet(*sheet_id),
4261                definition: named.definition.clone(),
4262            });
4263        }
4264
4265        out.sort_by(|a, b| {
4266            let a_scope = match a.scope {
4267                NameScope::Workbook => (0u8, 0u32),
4268                NameScope::Sheet(id) => (1u8, u32::from(id)),
4269            };
4270            let b_scope = match b.scope {
4271                NameScope::Workbook => (0u8, 0u32),
4272                NameScope::Sheet(id) => (1u8, u32::from(id)),
4273            };
4274            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
4275        });
4276
4277        out
4278    }
4279
4280    pub fn named_ranges_snapshot_for_sheet(
4281        &self,
4282        sheet_id: SheetId,
4283    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4284        self.named_ranges_snapshot()
4285            .into_iter()
4286            .filter(|entry| match entry.scope {
4287                NameScope::Workbook => true,
4288                NameScope::Sheet(id) => id == sheet_id,
4289            })
4290            .collect()
4291    }
4292
4293    pub fn define_name(
4294        &mut self,
4295        name: &str,
4296        definition: NamedDefinition,
4297        scope: NameScope,
4298    ) -> Result<(), ExcelError> {
4299        self.graph.validate_define_name(name, scope)?;
4300        self.graph.define_name(name, definition, scope)?;
4301        self.record_structural_change(StructuralScope::AllSheets);
4302
4303        self.mark_topology_edited();
4304
4305        Ok(())
4306    }
4307
4308    pub fn update_name(
4309        &mut self,
4310        name: &str,
4311        definition: NamedDefinition,
4312        scope: NameScope,
4313    ) -> Result<(), ExcelError> {
4314        self.graph.validate_existing_name(name, scope)?;
4315        self.graph.update_name(name, definition, scope)?;
4316        self.record_structural_change(StructuralScope::AllSheets);
4317
4318        self.mark_topology_edited();
4319
4320        Ok(())
4321    }
4322
4323    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
4324        self.graph.validate_existing_name(name, scope)?;
4325        self.graph.delete_name(name, scope)?;
4326        self.record_structural_change(StructuralScope::AllSheets);
4327
4328        self.mark_topology_edited();
4329
4330        Ok(())
4331    }
4332
4333    pub fn define_table(
4334        &mut self,
4335        name: &str,
4336        range: crate::reference::RangeRef,
4337        header_row: bool,
4338        headers: Vec<String>,
4339        totals_row: bool,
4340    ) -> Result<(), ExcelError> {
4341        self.graph
4342            .define_table(name, range, header_row, headers, totals_row)?;
4343        self.record_structural_change(StructuralScope::AllSheets);
4344        self.mark_topology_edited();
4345        Ok(())
4346    }
4347
4348    pub fn define_source_scalar(
4349        &mut self,
4350        name: &str,
4351        version: Option<u64>,
4352    ) -> Result<(), ExcelError> {
4353        self.graph.define_source_scalar(name, version)?;
4354        self.record_structural_change(StructuralScope::OpaqueGlobal);
4355        self.mark_topology_edited();
4356        Ok(())
4357    }
4358
4359    pub fn define_source_table(
4360        &mut self,
4361        name: &str,
4362        version: Option<u64>,
4363    ) -> Result<(), ExcelError> {
4364        self.graph.define_source_table(name, version)?;
4365        self.record_structural_change(StructuralScope::OpaqueGlobal);
4366        self.mark_topology_edited();
4367        Ok(())
4368    }
4369
4370    pub fn set_source_scalar_version(
4371        &mut self,
4372        name: &str,
4373        version: Option<u64>,
4374    ) -> Result<(), ExcelError> {
4375        self.graph.set_source_scalar_version(name, version)?;
4376        Ok(())
4377    }
4378
4379    pub fn set_source_table_version(
4380        &mut self,
4381        name: &str,
4382        version: Option<u64>,
4383    ) -> Result<(), ExcelError> {
4384        self.graph.set_source_table_version(name, version)?;
4385        Ok(())
4386    }
4387
4388    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
4389        self.graph.invalidate_source(name)?;
4390        Ok(())
4391    }
4392
4393    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
4394        self.graph.get_value(vertex)
4395    }
4396
4397    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
4398        self.graph.get_cell_value(sheet, row, col)
4399    }
4400
4401    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
4402        self.graph.get_vertex_for_cell(cell)
4403    }
4404
4405    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
4406        self.graph.get_evaluation_vertices()
4407    }
4408
4409    /// Return read-only baseline counters for dispatch benchmarking.
4410    pub fn baseline_stats(&self) -> EngineBaselineStats {
4411        let graph = self.graph.baseline_stats();
4412        EngineBaselineStats {
4413            graph_vertex_count: graph.graph_vertex_count,
4414            graph_formula_vertex_count: graph.graph_formula_vertex_count,
4415            graph_edge_count: graph.graph_edge_count,
4416            dirty_vertex_count: graph.dirty_vertex_count,
4417            evaluation_vertex_count: graph.evaluation_vertex_count,
4418            formula_ast_root_count: graph.formula_ast_root_count,
4419            formula_ast_node_count: graph.formula_ast_node_count,
4420            staged_formula_count: self.staged_formula_count(),
4421            formula_plane_active_span_count: 0,
4422            formula_plane_producer_result_entries: 0,
4423            formula_plane_consumer_read_entries: 0,
4424            formula_plane_mixed_topology_cache_builds: 0,
4425            formula_plane_mixed_topology_cache_hits: 0,
4426            formula_plane_mixed_topology_cache_overflows: 0,
4427            formula_plane_dirty_pending_events: 0,
4428            formula_plane_dirty_region_events_recorded: 0,
4429            formula_plane_dirty_span_region_events_recorded: 0,
4430            formula_plane_dirty_whole_span_seeds_recorded: 0,
4431            formula_plane_dirty_global_invalidations: 0,
4432            formula_plane_structural_span_candidates: 0,
4433            formula_plane_cycle_member_span_demotions: 0,
4434            formula_plane_array_result_span_demotions: 0,
4435            retained_scc_members: self.retained_scc_members.len(),
4436        }
4437    }
4438
4439    /// Mutation revision captured by read-only engine reports.
4440    pub(crate) fn inspection_mutation_revision(&self) -> u64 {
4441        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
4442    }
4443
4444    #[cfg(test)]
4445    pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
4446        self.used_axis_bounds_cache
4447            .read()
4448            .ok()
4449            .and_then(|guard| {
4450                guard.as_ref().map(|cache| {
4451                    (
4452                        cache.row_hits.load(Ordering::Relaxed),
4453                        cache.row_misses.load(Ordering::Relaxed),
4454                        cache.col_hits.load(Ordering::Relaxed),
4455                        cache.col_misses.load(Ordering::Relaxed),
4456                    )
4457                })
4458            })
4459            .unwrap_or((0, 0, 0, 0))
4460    }
4461
4462    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
4463        self.graph.set_first_load_assume_new(enabled);
4464    }
4465
4466    pub fn first_load_assume_new(&self) -> bool {
4467        self.graph.first_load_assume_new()
4468    }
4469
4470    pub fn reset_ensure_touched(&mut self) {
4471        self.graph.reset_ensure_touched();
4472    }
4473
4474    pub fn finalize_sheet_index(&mut self, sheet: &str) {
4475        self.graph.finalize_sheet_index(sheet);
4476    }
4477
4478    /// Execute a named Engine action.
4479    ///
4480    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
4481    /// For now actions are commit-only: they do not create changelog boundaries and they do not
4482    /// provide rollback/atomicity.
4483    ///
4484    /// Nested actions are deterministically handled by *disallowing* nesting: calling
4485    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
4486    pub fn action<T>(
4487        &mut self,
4488        name: impl AsRef<str>,
4489        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4490    ) -> Result<T, crate::engine::EditorError> {
4491        if self.action_depth != 0 {
4492            return Err(crate::engine::EditorError::TransactionFailed {
4493                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
4494                    .to_string(),
4495            });
4496        }
4497
4498        self.action_depth = 1;
4499        let engine_ptr: *mut Engine<R> = self;
4500        let _guard = ActionDepthGuard {
4501            engine: engine_ptr,
4502            _marker: std::marker::PhantomData,
4503        };
4504
4505        let mut tx = EngineAction {
4506            engine: self,
4507            name: name.as_ref().to_string(),
4508            capture: None,
4509            arrow_undo: None,
4510            atomic_policy: false,
4511        };
4512        f(&mut tx)
4513    }
4514
4515    /// Execute a named Engine action with atomic commit/rollback semantics.
4516    ///
4517    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
4518    pub fn action_atomic<T>(
4519        &mut self,
4520        name: impl Into<String>,
4521        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4522    ) -> Result<T, crate::engine::EditorError> {
4523        let (v, _j) = self.action_atomic_journal(name, f)?;
4524        Ok(v)
4525    }
4526
4527    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
4528    pub fn action_atomic_journal<T>(
4529        &mut self,
4530        name: impl Into<String>,
4531        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4532    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4533        if self.action_depth != 0 {
4534            return Err(crate::engine::EditorError::TransactionFailed {
4535                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4536                    .to_string(),
4537            });
4538        }
4539
4540        self.action_depth = 1;
4541        let engine_ptr: *mut Engine<R> = self;
4542        let _guard = ActionDepthGuard {
4543            engine: engine_ptr,
4544            _marker: std::marker::PhantomData,
4545        };
4546
4547        let name_str = name.into();
4548        let mut capture = MutationCapture::new(Default::default());
4549        let start_len = capture.len();
4550        self.action_atomic_impl(&mut capture, start_len, true, name_str, f)
4551    }
4552
4553    fn action_atomic_impl<T>(
4554        &mut self,
4555        capture: &mut MutationCapture,
4556        start_len: usize,
4557        expand_runs: bool,
4558        name: String,
4559        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4560    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4561        let invalidation_baseline = self.invalidation_baseline();
4562        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
4563        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
4564
4565        let capture_ptr: *mut MutationCapture = capture;
4566        let mut tx = EngineAction {
4567            engine: self,
4568            name: name.clone(),
4569            capture: Some(capture_ptr),
4570            arrow_undo: Some(arrow_ptr),
4571            atomic_policy: true,
4572        };
4573
4574        let res = f(&mut tx);
4575
4576        // Capture graph structural delta for this action. The journal is a
4577        // public value: run records (Program 2) are expanded into it, except
4578        // for a caller that only publishes the capture to its change log
4579        // (`expand_runs` false), where the journal (plain events) drives
4580        // invalidation and records count as topology changes; a rollback
4581        // still replays the expanded events.
4582        let capture_ref = unsafe { &*capture_ptr };
4583        let has_runs = capture_ref.lazy_len() > 0;
4584        let graph_events: Vec<crate::engine::ChangeEvent> = if expand_runs || res.is_err() {
4585            capture_ref.expanded_events_from(start_len, 0)
4586        } else {
4587            capture_ref.events()[start_len..].to_vec()
4588        };
4589        let graph_batch = crate::engine::GraphUndoBatch {
4590            events: graph_events,
4591        };
4592        let affected_cells = arrow_undo.ops.len();
4593        let journal = crate::engine::ActionJournal {
4594            name,
4595            graph: graph_batch,
4596            arrow: arrow_undo,
4597            affected_cells,
4598        };
4599
4600        match res {
4601            Ok(v) => {
4602                if !journal.graph.is_empty() || !journal.arrow.is_empty() || has_runs {
4603                    for event in &journal.graph.events {
4604                        self.record_change_for_event(event);
4605                    }
4606                    let mut impact = Self::classify_change_events(
4607                        &journal.graph.events,
4608                        LoggedEditDirection::Original,
4609                    )
4610                    .max(Self::classify_arrow_undo(&journal.arrow));
4611                    if has_runs {
4612                        impact = impact.max(LoggedEditImpact::Topology);
4613                    }
4614                    self.apply_logged_edit_impact(impact, invalidation_baseline);
4615                }
4616                Ok((v, journal))
4617            }
4618            Err(e) => {
4619                if let Err(rb) = self.rollback_from_action_journal(&journal, invalidation_baseline)
4620                {
4621                    return Err(crate::engine::EditorError::TransactionFailed {
4622                        reason: format!(
4623                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
4624                        ),
4625                    });
4626                }
4627                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
4628                    for event in &journal.graph.events {
4629                        self.record_change_for_event(event);
4630                    }
4631                }
4632                Err(e)
4633            }
4634        }
4635    }
4636
4637    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
4638    ///
4639    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
4640    /// - Dependency graph structural edits (via inverse ChangeEvents)
4641    /// - Arrow-truth overlay writes mirrored from ChangeEvents
4642    /// - ChangeLog entries (published only after a successful commit)
4643    pub fn action_with_logger<T>(
4644        &mut self,
4645        log: &mut crate::engine::ChangeLog,
4646        name: impl AsRef<str>,
4647        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4648    ) -> Result<T, crate::engine::EditorError> {
4649        if self.action_depth != 0 {
4650            return Err(crate::engine::EditorError::TransactionFailed {
4651                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4652                    .to_string(),
4653            });
4654        }
4655
4656        self.action_depth = 1;
4657        let engine_ptr: *mut Engine<R> = self;
4658        let _guard = ActionDepthGuard {
4659            engine: engine_ptr,
4660            _marker: std::marker::PhantomData,
4661        };
4662
4663        let name_str = name.as_ref().to_string();
4664        let mut capture = MutationCapture::new(log.current_meta());
4665        let start_len = capture.len();
4666        capture.begin_compound(name_str.clone());
4667
4668        // Mutation correctness uses the complete private capture. The provided ChangeLog remains
4669        // an observability sink and is not touched until the action outcome is known.
4670        let res = self.action_atomic_impl(&mut capture, start_len, false, name_str, f);
4671        capture.close_compounds();
4672
4673        match res {
4674            Ok((v, _journal)) => {
4675                log.publish_capture(capture);
4676                Ok(v)
4677            }
4678            Err(e) => {
4679                // Preserve sequence/group gaps without retaining failed events or evicting history.
4680                log.discard_capture(capture);
4681                Err(e)
4682            }
4683        }
4684    }
4685
4686    fn rollback_from_action_journal(
4687        &mut self,
4688        journal: &crate::engine::ActionJournal,
4689        invalidation_baseline: InvalidationBaseline,
4690    ) -> Result<(), crate::engine::EditorError> {
4691        // Invalidate first so a partial inverse failure cannot leave a changed
4692        // graph behind an apparently current schedule or lookup cache.
4693        self.invalidate_for_action_journal(
4694            journal,
4695            LoggedEditDirection::InverseReplay,
4696            invalidation_baseline,
4697        );
4698        // 1) Roll back the dependency graph structure.
4699        journal.graph.undo(&mut self.graph)?;
4700        // 2) Roll back engine row-visibility sidecar events.
4701        self.apply_inverse_row_visibility_events(&journal.graph.events);
4702        // 3) Roll back Arrow-truth overlays.
4703        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
4704        Ok(())
4705    }
4706
4707    fn rollback_from_change_events(
4708        &mut self,
4709        events: &[crate::engine::ChangeEvent],
4710        invalidation_baseline: InvalidationBaseline,
4711    ) -> Result<(), crate::engine::EditorError> {
4712        use crate::engine::ChangeEvent;
4713
4714        // Fail closed before applying inverses because replay can return after
4715        // only part of the batch has been restored.
4716        self.invalidate_for_change_events(
4717            events,
4718            LoggedEditDirection::InverseReplay,
4719            invalidation_baseline,
4720        );
4721
4722        // 1) Roll back the dependency graph.
4723        self.graph
4724            .authority_set_replay(crate::engine::authority::history::Replay::Undo);
4725        let rolled_back = (|| {
4726            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
4727            let mut compound_stack: Vec<usize> = Vec::new();
4728            for (i, ev) in events.iter().enumerate().rev() {
4729                match ev {
4730                    ChangeEvent::CompoundEnd { depth } => {
4731                        compound_stack.push(*depth);
4732                        if let Some(description) =
4733                            crate::engine::graph::editor::change_log::compound_start_description(
4734                                i,
4735                                |j| &events[j],
4736                            )
4737                        {
4738                            editor.inverse_compound_end(description);
4739                        }
4740                    }
4741                    ChangeEvent::CompoundStart { depth, .. } => {
4742                        if compound_stack.last() == Some(depth) {
4743                            compound_stack.pop();
4744                        }
4745                        editor.apply_inverse(ev.clone())?;
4746                    }
4747                    ChangeEvent::SetRowVisibility { .. } => {
4748                        // Engine-side metadata handled after dropping graph editor borrow.
4749                    }
4750                    _ => {
4751                        editor.apply_inverse(ev.clone())?;
4752                    }
4753                }
4754            }
4755            Ok::<_, crate::engine::EditorError>(())
4756        })();
4757        self.graph
4758            .authority_set_replay(crate::engine::authority::history::Replay::Forward);
4759        rolled_back?;
4760
4761        // 2) Roll back engine row-visibility metadata.
4762        for ev in events.iter().rev() {
4763            self.apply_inverse_row_visibility_event(ev);
4764        }
4765
4766        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
4767        for ev in events.iter().rev() {
4768            self.mirror_inverse_change_to_arrow(ev);
4769        }
4770
4771        Ok(())
4772    }
4773
4774    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
4775        let sheet_id = self.graph.sheet_id(sheet)?;
4776        let coord = Coord::from_excel(row, col, true, true);
4777        let cell = CellRef::new(sheet_id, coord);
4778        let vid = self.graph.get_vertex_for_cell(&cell)?;
4779        self.graph.get_formula(vid)
4780    }
4781
4782    pub fn define_name_with_logger(
4783        &mut self,
4784        log: &mut crate::engine::ChangeLog,
4785        name: &str,
4786        definition: NamedDefinition,
4787        scope: NameScope,
4788    ) -> Result<(), crate::engine::EditorError> {
4789        self.graph
4790            .validate_define_name(name, scope)
4791            .map_err(crate::engine::EditorError::Excel)?;
4792
4793        {
4794            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4795            editor.define_name(name, definition, scope)?;
4796        }
4797        self.record_structural_change(StructuralScope::AllSheets);
4798
4799        self.mark_topology_edited();
4800
4801        Ok(())
4802    }
4803
4804    pub fn update_name_with_logger(
4805        &mut self,
4806        log: &mut crate::engine::ChangeLog,
4807        name: &str,
4808        definition: NamedDefinition,
4809        scope: NameScope,
4810    ) -> Result<(), crate::engine::EditorError> {
4811        self.graph
4812            .validate_existing_name(name, scope)
4813            .map_err(crate::engine::EditorError::Excel)?;
4814        {
4815            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4816            editor.update_name(name, definition, scope)?;
4817        }
4818        self.record_structural_change(StructuralScope::AllSheets);
4819
4820        self.mark_topology_edited();
4821
4822        Ok(())
4823    }
4824
4825    pub fn delete_name_with_logger(
4826        &mut self,
4827        log: &mut crate::engine::ChangeLog,
4828        name: &str,
4829        scope: NameScope,
4830    ) -> Result<(), crate::engine::EditorError> {
4831        self.graph
4832            .validate_existing_name(name, scope)
4833            .map_err(crate::engine::EditorError::Excel)?;
4834        {
4835            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
4836            editor.delete_name(name, scope)?;
4837        }
4838        self.record_structural_change(StructuralScope::AllSheets);
4839
4840        self.mark_topology_edited();
4841
4842        Ok(())
4843    }
4844
4845    pub fn edit_with_logger<T>(
4846        &mut self,
4847        log: &mut crate::engine::ChangeLog,
4848        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
4849    ) -> Result<T, crate::engine::EditorError> {
4850        let mut capture = MutationCapture::new(log.current_meta());
4851        let result = self.edit_with_capture(&mut capture, f);
4852        capture.close_compounds();
4853        match result {
4854            Ok(value) => {
4855                log.publish_capture(capture);
4856                Ok(value)
4857            }
4858            Err(error) => {
4859                log.discard_capture(capture);
4860                Err(error)
4861            }
4862        }
4863    }
4864
4865    fn edit_with_capture<T>(
4866        &mut self,
4867        capture: &mut MutationCapture,
4868        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
4869    ) -> Result<T, crate::engine::EditorError> {
4870        let invalidation_baseline = self.invalidation_baseline();
4871        let start_len = capture.len();
4872        let lazy_start = capture.lazy_len();
4873
4874        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
4875        // (graph value cache is intentionally empty in canonical mode).
4876        struct ArrowSpillReader<'a> {
4877            sheets: &'a crate::arrow_store::SheetStore,
4878        }
4879        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
4880            fn read_cell_value(
4881                &self,
4882                sheet: &str,
4883                row: u32,
4884                col: u32,
4885            ) -> Option<formualizer_common::LiteralValue> {
4886                use formualizer_common::LiteralValue;
4887                let asheet = self.sheets.sheet(sheet)?;
4888                let r0 = row.saturating_sub(1) as usize;
4889                let c0 = col.saturating_sub(1) as usize;
4890                let v = asheet.get_cell_value(r0, c0);
4891                if matches!(v, LiteralValue::Empty) {
4892                    None
4893                } else {
4894                    Some(v)
4895                }
4896            }
4897        }
4898
4899        let ret = {
4900            let spill_reader = ArrowSpillReader {
4901                sheets: &self.arrow_sheets,
4902            };
4903            let mut editor = crate::engine::VertexEditor::with_capture_and_spill_reader(
4904                &mut self.graph,
4905                capture,
4906                &spill_reader,
4907            );
4908            f(&mut editor)
4909        };
4910
4911        // Plain events only: run records (Program 2) stand for
4912        // `FormulaAdjusted` events, which have no forward effect here but
4913        // topology invalidation.
4914        let new_events = capture.events()[start_len..].to_vec();
4915        let new_runs = capture.lazy_len() > lazy_start;
4916        if new_events.iter().any(|event| {
4917            matches!(
4918                event,
4919                ChangeEvent::DefineName { .. }
4920                    | ChangeEvent::UpdateName { .. }
4921                    | ChangeEvent::DeleteName { .. }
4922            )
4923        }) {
4924            let all = capture.expanded_events_from(start_len, lazy_start);
4925            self.rollback_from_change_events(&all, invalidation_baseline)?;
4926            return Err(crate::engine::EditorError::TransactionUnsupported {
4927                reason: "name mutations must use Engine's prepared logged-name APIs".to_string(),
4928            });
4929        }
4930
4931        // Mirror value-impacting graph events to Arrow for forward edits.
4932        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
4933        self.clear_logged_cell_format_states(&new_events);
4934        for ev in &new_events {
4935            self.mirror_forward_change_to_arrow(ev);
4936        }
4937        for ev in &new_events {
4938            self.record_change_for_event(ev);
4939        }
4940
4941        // Atomic EngineAction calls publish one invalidation for their complete
4942        // journal at commit/rollback. Direct logged edits publish here.
4943        if self.action_depth == 0 {
4944            let mut impact =
4945                Self::classify_change_events(&new_events, LoggedEditDirection::Original);
4946            if new_runs {
4947                impact = impact.max(LoggedEditImpact::Topology);
4948            }
4949            self.apply_logged_edit_impact(impact, invalidation_baseline);
4950        }
4951
4952        Ok(ret)
4953    }
4954
4955    pub(crate) fn preflight_replay_admission(
4956        &mut self,
4957        events: &[ChangeEvent],
4958        forward: bool,
4959    ) -> Result<(), crate::engine::EditorError> {
4960        if !self.graph_admission_enabled() {
4961            return Ok(());
4962        }
4963        let mut vertex_delta = 0i128;
4964        let mut edge_delta = 0i128;
4965        let mut added_vertices = 0usize;
4966        let mut added_edges = 0usize;
4967        let mut formula_cells = BTreeSet::new();
4968        for event in events {
4969            match event {
4970                ChangeEvent::AddVertex {
4971                    formula,
4972                    coord,
4973                    sheet_id,
4974                    ..
4975                } => {
4976                    let delta = if forward { 1 } else { -1 };
4977                    vertex_delta += delta;
4978                    if forward {
4979                        added_vertices = added_vertices.saturating_add(1);
4980                        if formula.is_some() {
4981                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
4982                        }
4983                    }
4984                }
4985                ChangeEvent::RemoveVertex {
4986                    old_formula,
4987                    coord,
4988                    sheet_id,
4989                    ..
4990                } => {
4991                    let delta = if forward { -1 } else { 1 };
4992                    vertex_delta += delta;
4993                    if !forward {
4994                        added_vertices = added_vertices.saturating_add(1);
4995                        if old_formula.is_some()
4996                            && let (Some(sheet_id), Some(coord)) = (sheet_id, coord)
4997                        {
4998                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
4999                        }
5000                    }
5001                }
5002                ChangeEvent::EdgeAdded { .. } => {
5003                    let delta = if forward { 1 } else { -1 };
5004                    edge_delta += delta;
5005                    if forward {
5006                        added_edges = added_edges.saturating_add(1);
5007                    }
5008                }
5009                ChangeEvent::EdgeRemoved { .. } => {
5010                    let delta = if forward { -1 } else { 1 };
5011                    edge_delta += delta;
5012                    if !forward {
5013                        added_edges = added_edges.saturating_add(1);
5014                    }
5015                }
5016                ChangeEvent::SetFormula {
5017                    addr, old_formula, ..
5018                } => {
5019                    if forward || old_formula.is_some() {
5020                        formula_cells.insert((addr.sheet_id, addr.coord.row(), addr.coord.col()));
5021                    }
5022                }
5023                _ => {}
5024            }
5025        }
5026        let stats = self.graph.baseline_stats();
5027        let final_vertices = i128::try_from(stats.graph_vertex_count)
5028            .ok()
5029            .and_then(|count| count.checked_add(vertex_delta))
5030            .and_then(|count| usize::try_from(count).ok())
5031            .ok_or_else(|| {
5032                crate::engine::EditorError::Excel(
5033                    ExcelError::new(ExcelErrorKind::NImpl)
5034                        .with_message("replay vertex count overflow"),
5035                )
5036            })?;
5037        let final_edges = i128::try_from(stats.graph_edge_count)
5038            .ok()
5039            .and_then(|count| count.checked_add(edge_delta))
5040            .and_then(|count| usize::try_from(count).ok())
5041            .ok_or_else(|| {
5042                crate::engine::EditorError::Excel(
5043                    ExcelError::new(ExcelErrorKind::NImpl)
5044                        .with_message("replay edge count overflow"),
5045                )
5046            })?;
5047        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
5048            final_vertices,
5049            final_edges,
5050            materialization_cells: formula_cells.len() as u64,
5051            added_vertices,
5052            added_edges,
5053        })
5054        .map_err(crate::engine::EditorError::Excel)
5055    }
5056
5057    /// Undo the last group still retained by the provided audit log.
5058    ///
5059    /// Disabled, zero-cap, and evicted history is unavailable on this index-based path. Use an
5060    /// explicit `ActionJournal` with `undo_action` when undo must be independent of audit retention.
5061    pub fn undo_logged(
5062        &mut self,
5063        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5064        log: &mut crate::engine::ChangeLog,
5065    ) -> Result<(), crate::engine::EditorError> {
5066        let pending_events = log
5067            .last_group_indices()
5068            .into_iter()
5069            .map(|index| log.events()[index].clone())
5070            .collect::<Vec<_>>();
5071        self.preflight_replay_admission(&pending_events, false)?;
5072        let invalidation_baseline = self.invalidation_baseline();
5073        // UndoEngine can fail after partially applying the batch, so publish
5074        // invalidation before replay rather than only on the success path.
5075        self.invalidate_for_change_events(
5076            &pending_events,
5077            LoggedEditDirection::InverseReplay,
5078            invalidation_baseline,
5079        );
5080        let batch = undo.undo(&mut self.graph, log)?;
5081        for item in batch.iter().rev() {
5082            self.apply_inverse_row_visibility_event(&item.event);
5083            self.apply_inverse_staged_formula_event(&item.event);
5084        }
5085        if !batch.is_empty() {
5086            let events = batch
5087                .iter()
5088                .map(|item| item.event.clone())
5089                .collect::<Vec<_>>();
5090            self.clear_logged_cell_format_states(&events);
5091        }
5092        self.mirror_undo_batch_to_arrow(&batch);
5093        if !batch.is_empty() {
5094            for item in &batch {
5095                self.record_change_for_event(&item.event);
5096            }
5097        }
5098        crate::engine::trace::fz_event!(
5099            tracing::Level::INFO,
5100            "history",
5101            "history.replay",
5102            op = "undo",
5103            events_replayed = batch.len(),
5104            ownership_resyncs = batch.len()
5105        );
5106        Ok(())
5107    }
5108
5109    pub fn redo_logged(
5110        &mut self,
5111        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5112        log: &mut crate::engine::ChangeLog,
5113    ) -> Result<(), crate::engine::EditorError> {
5114        let pending_events = undo.pending_redo_events();
5115        self.preflight_replay_admission(&pending_events, true)?;
5116        let invalidation_baseline = self.invalidation_baseline();
5117        self.invalidate_for_change_events(
5118            &pending_events,
5119            LoggedEditDirection::ForwardReplay,
5120            invalidation_baseline,
5121        );
5122        let batch = undo.redo(&mut self.graph, log)?;
5123        for item in &batch {
5124            self.apply_forward_row_visibility_event(&item.event);
5125            self.apply_forward_staged_formula_event(&item.event);
5126        }
5127        if !batch.is_empty() {
5128            let events = batch
5129                .iter()
5130                .map(|item| item.event.clone())
5131                .collect::<Vec<_>>();
5132            self.clear_logged_cell_format_states(&events);
5133        }
5134        self.mirror_redo_batch_to_arrow(&batch);
5135        if !batch.is_empty() {
5136            for item in &batch {
5137                self.record_change_for_event(&item.event);
5138            }
5139        }
5140        crate::engine::trace::fz_event!(
5141            tracing::Level::INFO,
5142            "history",
5143            "history.replay",
5144            op = "redo",
5145            events_replayed = batch.len(),
5146            ownership_resyncs = batch.len()
5147        );
5148        Ok(())
5149    }
5150
5151    /// Undo the last committed atomic action using the journal stack.
5152    ///
5153    /// This path does not require a `ChangeLog`.
5154    pub fn undo_action(
5155        &mut self,
5156        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5157    ) -> Result<(), crate::engine::EditorError> {
5158        let Some(journal) = undo.pop_undo_action() else {
5159            return Ok(());
5160        };
5161        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, false) {
5162            undo.push_done_action(journal);
5163            return Err(error);
5164        }
5165        let invalidation_baseline = self.invalidation_baseline();
5166
5167        self.invalidate_for_action_journal(
5168            &journal,
5169            LoggedEditDirection::InverseReplay,
5170            invalidation_baseline,
5171        );
5172        journal.graph.undo(&mut self.graph)?;
5173        self.apply_inverse_row_visibility_events(&journal.graph.events);
5174        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
5175        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5176            for event in &journal.graph.events {
5177                self.record_change_for_event(event);
5178            }
5179        }
5180
5181        #[cfg(feature = "tracing")]
5182        let events_replayed = journal.graph.events.len();
5183        crate::engine::trace::fz_event!(
5184            tracing::Level::INFO,
5185            "history",
5186            "history.replay",
5187            op = "undo",
5188            events_replayed,
5189            ownership_resyncs = events_replayed
5190        );
5191        undo.push_redo_action(journal);
5192        Ok(())
5193    }
5194
5195    /// Redo the last undone atomic action using the journal stack.
5196    ///
5197    /// This path does not require a `ChangeLog`.
5198    pub fn redo_action(
5199        &mut self,
5200        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5201    ) -> Result<(), crate::engine::EditorError> {
5202        let Some(journal) = undo.pop_redo_action() else {
5203            return Ok(());
5204        };
5205        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, true) {
5206            undo.push_redo_action(journal);
5207            return Err(error);
5208        }
5209        let invalidation_baseline = self.invalidation_baseline();
5210        self.invalidate_for_action_journal(
5211            &journal,
5212            LoggedEditDirection::ForwardReplay,
5213            invalidation_baseline,
5214        );
5215        journal.graph.redo(&mut self.graph)?;
5216        self.apply_forward_row_visibility_events(&journal.graph.events);
5217        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
5218        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5219            for event in &journal.graph.events {
5220                self.record_change_for_event(event);
5221            }
5222        }
5223
5224        #[cfg(feature = "tracing")]
5225        let events_replayed = journal.graph.events.len();
5226        crate::engine::trace::fz_event!(
5227            tracing::Level::INFO,
5228            "history",
5229            "history.replay",
5230            op = "redo",
5231            events_replayed,
5232            ownership_resyncs = events_replayed
5233        );
5234        undo.push_done_action(journal);
5235        Ok(())
5236    }
5237
5238    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
5239        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
5240        // Coord stores 0-based indices.
5241        let row = addr.coord.row() + 1;
5242        let col = addr.coord.col() + 1;
5243        (sheet, row, col)
5244    }
5245
5246    fn mirror_undo_batch_to_arrow(
5247        &mut self,
5248        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5249    ) {
5250        // Undo applies inverses in reverse order.
5251        for item in batch.iter().rev() {
5252            self.mirror_inverse_change_to_arrow(&item.event);
5253        }
5254    }
5255
5256    fn mirror_redo_batch_to_arrow(
5257        &mut self,
5258        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5259    ) {
5260        // Redo applies events in forward order.
5261        for item in batch.iter() {
5262            self.mirror_forward_change_to_arrow(&item.event);
5263        }
5264    }
5265
5266    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5267        use crate::engine::ChangeEvent;
5268        use formualizer_common::LiteralValue;
5269
5270        match ev {
5271            ChangeEvent::SetValue {
5272                addr,
5273                old_value,
5274                old_formula,
5275                ..
5276            } => {
5277                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5278                if old_formula.is_some() {
5279                    self.clear_delta_overlay_cell(&sheet, row, col);
5280                } else {
5281                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5282                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5283                }
5284            }
5285            ChangeEvent::SetFormula {
5286                addr,
5287                old_value,
5288                old_formula,
5289                ..
5290            } => {
5291                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5292                if old_formula.is_some() {
5293                    self.clear_delta_overlay_cell(&sheet, row, col);
5294                } else {
5295                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5296                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5297                }
5298            }
5299            ChangeEvent::SpillCommitted { old, new, .. } => {
5300                // Inverse: restore `old` (or clear if none).
5301                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
5302                if let Some(snap) = old {
5303                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
5304                }
5305            }
5306            ChangeEvent::SpillCleared { old, .. } => {
5307                // Inverse: restore prior spill.
5308                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
5309            }
5310            ChangeEvent::SetRowVisibility { .. } => {
5311                // Engine-side metadata only; no Arrow overlay effect.
5312            }
5313            _ => {}
5314        }
5315    }
5316
5317    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5318        use crate::engine::ChangeEvent;
5319
5320        match ev {
5321            ChangeEvent::SetValue { addr, new, .. } => {
5322                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5323                self.mirror_value_to_overlay(&sheet, row, col, new);
5324            }
5325            ChangeEvent::SetFormula { addr, .. } => {
5326                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5327                self.clear_delta_overlay_cell(&sheet, row, col);
5328                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
5329            }
5330            ChangeEvent::SpillCommitted { old, new, .. } => {
5331                if let Some(snap) = old {
5332                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
5333                }
5334                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
5335            }
5336            ChangeEvent::SpillCleared { old, .. } => {
5337                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
5338            }
5339            ChangeEvent::SetRowVisibility { .. } => {
5340                // Engine-side metadata only; no Arrow overlay effect.
5341            }
5342            _ => {
5343                // Other graph structural operations do not have direct value effects in Arrow.
5344            }
5345        }
5346    }
5347
5348    fn mirror_spill_snapshot(
5349        &mut self,
5350        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
5351        clear_only: bool,
5352    ) {
5353        use formualizer_common::LiteralValue;
5354
5355        let mut i = 0usize;
5356        for row in &snap.values {
5357            for v in row {
5358                if let Some(cell) = snap.target_cells.get(i) {
5359                    let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5360                    let out = if clear_only {
5361                        LiteralValue::Empty
5362                    } else {
5363                        v.clone()
5364                    };
5365                    self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
5366                }
5367                i += 1;
5368            }
5369        }
5370        // If target_cells is longer than values (should not happen), clear remaining cells.
5371        if clear_only {
5372            for cell in snap.target_cells.iter().skip(i) {
5373                let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5374                self.mirror_value_to_computed_overlay(&sheet, r, c, &LiteralValue::Empty);
5375            }
5376        }
5377    }
5378
5379    pub fn set_default_sheet_by_name(&mut self, name: &str) {
5380        self.graph.set_default_sheet_by_name(name);
5381    }
5382
5383    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
5384        self.graph.set_default_sheet_by_id(id);
5385    }
5386
5387    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
5388        self.graph.set_sheet_index_mode(mode);
5389    }
5390
5391    #[cfg(feature = "test-support")]
5392    #[doc(hidden)]
5393    pub fn mark_all_formulas_dirty_for_test(&mut self) {
5394        self.mark_all_formula_vertices_dirty();
5395    }
5396
5397    #[cfg(feature = "test-support")]
5398    #[doc(hidden)]
5399    pub fn take_criteria_mask_work_for_test() -> (usize, usize) {
5400        criteria_mask_test_hooks::take_mask_work()
5401    }
5402
5403    #[cfg(feature = "test-support")]
5404    #[doc(hidden)]
5405    pub fn lookup_index_cache_report_for_test(&self) -> LookupIndexCacheReport {
5406        self.lookup_index_cache.report()
5407    }
5408
5409    #[cfg(any(test, feature = "benchmark_internal"))]
5410    #[doc(hidden)]
5411    pub fn reset_recalc_reuse_probe(&mut self) {
5412        *self.recalc_reuse_probe.get_mut().unwrap() = RecalcReuseProbe::default();
5413    }
5414
5415    #[cfg(any(test, feature = "benchmark_internal"))]
5416    #[doc(hidden)]
5417    pub fn recalc_reuse_probe(&self) -> RecalcReuseProbe {
5418        let mut probe = self.recalc_reuse_probe.lock().unwrap().clone();
5419        if let Some(cached) = self.cached_static_schedule.as_ref() {
5420            let entry_bytes = |e: &CachedScheduleEntry| {
5421                std::mem::size_of::<CachedScheduleEntry>()
5422                    + e.candidate_vertices.heap_bytes()
5423                    + std::mem::size_of::<crate::engine::Schedule>()
5424                    + 2 * std::mem::size_of::<usize>()
5425                    + schedule_probe_retained_bytes(&e.schedule)
5426            };
5427            probe.schedule_retained_bytes = entry_bytes(cached)
5428                + self.recent_schedules.iter().map(entry_bytes).sum::<usize>()
5429                + self.base_schedule.as_ref().map_or(0, entry_bytes)
5430                + self.recent_schedules.capacity() * std::mem::size_of::<CachedScheduleEntry>();
5431        }
5432        probe
5433    }
5434
5435    #[cfg(test)]
5436    pub(crate) fn cached_static_schedule_for_test(&self) -> Option<Arc<crate::engine::Schedule>> {
5437        self.cached_static_schedule
5438            .as_ref()
5439            .map(|cached| Arc::clone(&cached.schedule))
5440    }
5441
5442    fn clear_cached_static_schedule(&mut self) {
5443        self.cached_static_schedule = None;
5444        self.recent_schedules.clear();
5445        self.base_schedule = None;
5446    }
5447
5448    /// Keep a replaced schedule among the recent ones when it is still
5449    /// current and small; drop stale ones and the oldest beyond the bounds.
5450    fn retain_recent_schedule(&mut self, entry: CachedScheduleEntry) {
5451        const RECENT_SCHEDULES: usize = 8;
5452        const RECENT_SCHEDULE_VERTICES: usize = 65_536;
5453        let revision = self.schedule_cache_authority_revision();
5454        let epoch = self.topology_epoch;
5455        self.recent_schedules
5456            .retain(|e| e.topology_epoch == epoch && e.authority_revision == revision);
5457        let current =
5458            |e: &CachedScheduleEntry| e.topology_epoch == epoch && e.authority_revision == revision;
5459        if self.base_schedule.as_ref().is_some_and(|b| !current(b)) {
5460            self.base_schedule = None;
5461        }
5462        // The largest current schedule becomes the base; a replaced base
5463        // may still join the recent ones.
5464        let mut entry = entry;
5465        if current(&entry)
5466            && entry.candidate_vertices.len() > BASE_SCHEDULE_MIN_VERTICES
5467            && self
5468                .base_schedule
5469                .as_ref()
5470                .is_none_or(|b| entry.candidate_vertices.len() > b.candidate_vertices.len())
5471        {
5472            match self.base_schedule.replace(entry) {
5473                Some(previous) => entry = previous,
5474                None => return,
5475            }
5476        }
5477        if entry.topology_epoch != epoch
5478            || entry.authority_revision != revision
5479            || entry.candidate_vertices.len() > RECENT_SCHEDULE_VERTICES / 4
5480        {
5481            return;
5482        }
5483        self.recent_schedules.push(entry);
5484        let mut total: usize = self
5485            .recent_schedules
5486            .iter()
5487            .map(|e| e.candidate_vertices.len())
5488            .sum();
5489        while self.recent_schedules.len() > RECENT_SCHEDULES || total > RECENT_SCHEDULE_VERTICES {
5490            let oldest = self.recent_schedules.remove(0);
5491            total -= oldest.candidate_vertices.len();
5492        }
5493    }
5494
5495    fn invalidation_baseline(&self) -> InvalidationBaseline {
5496        InvalidationBaseline {
5497            snapshot_id: self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed),
5498            topology_epoch: self.topology_epoch,
5499        }
5500    }
5501
5502    fn classify_change_events(
5503        events: &[crate::engine::ChangeEvent],
5504        direction: LoggedEditDirection,
5505    ) -> LoggedEditImpact {
5506        use crate::engine::ChangeEvent;
5507
5508        events
5509            .iter()
5510            .map(|event| match event {
5511                ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
5512                    LoggedEditImpact::NoOp
5513                }
5514                ChangeEvent::SetRowVisibility { .. }
5515                | ChangeEvent::SetValue {
5516                    old_formula: None,
5517                    old_value: Some(_),
5518                    ..
5519                } => LoggedEditImpact::DataOnly,
5520                // Original canonical writes can lack an Arrow old value while
5521                // updating an existing placeholder. Inverse replay actually
5522                // removes that vertex; a later redo recreates it.
5523                ChangeEvent::SetValue {
5524                    old_formula: None,
5525                    old_value: None,
5526                    ..
5527                } if direction == LoggedEditDirection::Original => LoggedEditImpact::DataOnly,
5528                ChangeEvent::SetValue { .. }
5529                | ChangeEvent::SetFormula { .. }
5530                | ChangeEvent::AddVertex { .. }
5531                | ChangeEvent::RemoveVertex { .. }
5532                | ChangeEvent::VertexMoved { .. }
5533                | ChangeEvent::FormulaAdjusted { .. }
5534                | ChangeEvent::NamedRangeAdjusted { .. }
5535                | ChangeEvent::EdgeAdded { .. }
5536                | ChangeEvent::EdgeRemoved { .. }
5537                | ChangeEvent::DefineName { .. }
5538                | ChangeEvent::UpdateName { .. }
5539                | ChangeEvent::DeleteName { .. }
5540                | ChangeEvent::SpillCommitted { .. }
5541                | ChangeEvent::SpillCleared { .. }
5542                | ChangeEvent::StagedFormulaCellChanged { .. } => LoggedEditImpact::Topology,
5543            })
5544            .max()
5545            .unwrap_or(LoggedEditImpact::NoOp)
5546    }
5547
5548    fn classify_arrow_undo(arrow: &crate::engine::ArrowUndoBatch) -> LoggedEditImpact {
5549        use crate::engine::ArrowOp;
5550
5551        arrow
5552            .ops
5553            .iter()
5554            .map(|op| match op {
5555                ArrowOp::SetDeltaCell { .. } | ArrowOp::SetComputedCell { .. } => {
5556                    LoggedEditImpact::DataOnly
5557                }
5558                ArrowOp::RestoreComputedRect { .. }
5559                | ArrowOp::InsertRows { .. }
5560                | ArrowOp::InsertCols { .. } => LoggedEditImpact::Topology,
5561            })
5562            .max()
5563            .unwrap_or(LoggedEditImpact::NoOp)
5564    }
5565
5566    fn apply_logged_edit_impact(
5567        &mut self,
5568        impact: LoggedEditImpact,
5569        baseline: InvalidationBaseline,
5570    ) {
5571        match impact {
5572            LoggedEditImpact::NoOp => {}
5573            LoggedEditImpact::DataOnly => {
5574                if self.topology_epoch == baseline.topology_epoch
5575                    && self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
5576                        == baseline.snapshot_id
5577                {
5578                    self.mark_data_edited();
5579                }
5580            }
5581            LoggedEditImpact::Topology => {
5582                // Some structural entry points already publish topology
5583                // invalidation. Do not bump the same batch twice.
5584                if self.topology_epoch == baseline.topology_epoch {
5585                    self.mark_topology_edited();
5586                }
5587            }
5588        }
5589    }
5590
5591    fn invalidate_for_change_events(
5592        &mut self,
5593        events: &[crate::engine::ChangeEvent],
5594        direction: LoggedEditDirection,
5595        baseline: InvalidationBaseline,
5596    ) {
5597        self.apply_logged_edit_impact(Self::classify_change_events(events, direction), baseline);
5598    }
5599
5600    fn invalidate_for_action_journal(
5601        &mut self,
5602        journal: &crate::engine::ActionJournal,
5603        direction: LoggedEditDirection,
5604        baseline: InvalidationBaseline,
5605    ) {
5606        let impact = Self::classify_change_events(&journal.graph.events, direction)
5607            .max(Self::classify_arrow_undo(&journal.arrow));
5608        self.apply_logged_edit_impact(impact, baseline);
5609    }
5610
5611    /// Mark data edited: bump snapshot and set edited flag.
5612    /// Value-only edits keep the stable-topology schedule cache alive.
5613    pub fn mark_data_edited(&mut self) {
5614        self.lookup_index_cache.clear();
5615        self.snapshot_id
5616            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5617        self.has_edited = true;
5618    }
5619
5620    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
5621    pub fn mark_topology_edited(&mut self) {
5622        self.lookup_index_cache.clear();
5623        self.snapshot_id
5624            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5625        self.topology_epoch = self.topology_epoch.wrapping_add(1);
5626        self.graph.bump_topology_revision();
5627        self.clear_cached_static_schedule();
5628        if let Some(ledger) = self.active_resource_ledger.as_mut() {
5629            let released = ledger.account_mixed_cache(0);
5630            debug_assert!(released.is_ok());
5631        }
5632        self.has_edited = true;
5633        // Eager sync at a topology edit, so read-only (`&self`) plans and
5634        // inspection see a current authority (not during a load or an open
5635        // structural capture).
5636        self.graph.authority_sync_eager();
5637    }
5638
5639    fn mark_all_formula_vertices_dirty(&mut self) {
5640        let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
5641        for vertex in vertices {
5642            self.graph.mark_vertex_dirty(vertex);
5643        }
5644    }
5645
5646    fn mark_moved_formula_vertices_dirty(
5647        &mut self,
5648        summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
5649    ) {
5650        for vertex in &summary.vertices_moved {
5651            if self.graph.has_formula(*vertex) {
5652                self.graph.mark_vertex_dirty(*vertex);
5653            }
5654        }
5655    }
5656
5657    /// Access Arrow sheet store (read-only)
5658    pub fn sheet_store(&self) -> &SheetStore {
5659        &self.arrow_sheets
5660    }
5661
5662    /// True when any sheet carries manual/filter row-visibility state.
5663    /// Used by load-time freshness checks (see `Engine::adopt_file_sheets`).
5664    pub(crate) fn has_row_visibility_state(&self) -> bool {
5665        !self.row_visibility.is_empty()
5666    }
5667
5668    /// Access Arrow sheet store (mutable)
5669    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
5670        &mut self.arrow_sheets
5671    }
5672
5673    pub fn has_staged_formulas(&self) -> bool {
5674        !self.staged_formulas.is_empty()
5675    }
5676
5677    pub fn staged_formula_count(&self) -> usize {
5678        self.staged_formulas.values().map(StagedSheet::len).sum()
5679    }
5680
5681    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
5682    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
5683        self.staged_formulas
5684            .entry(sheet.to_string())
5685            .or_default()
5686            .stage(row, col, text);
5687        self.staged_formula_index.stage(sheet, row, col);
5688        if let Some(sheet) = self.graph.sheet_id(sheet) {
5689            self.invalidate_pending_spills(StructuralScope::Cell {
5690                sheet,
5691                row: row.saturating_sub(1),
5692                col: col.saturating_sub(1),
5693            });
5694        }
5695    }
5696
5697    fn index_removed_staged_sheet(&mut self, sheet: &str, staged: &StagedSheet) {
5698        for (row, col, _) in &staged.entries {
5699            self.staged_formula_index.remove(sheet, *row, *col);
5700        }
5701        if staged.deferred_package.is_some() {
5702            self.staged_formula_index.set_package(sheet, None);
5703        }
5704    }
5705
5706    fn restore_staged_sheet(&mut self, sheet: String, staged: StagedSheet) {
5707        self.staged_formulas.insert(sheet, staged);
5708    }
5709
5710    #[doc(hidden)]
5711    pub fn source_formula_ingress(&mut self) -> SourceFormulaIngress<'_, R> {
5712        SourceFormulaIngress { engine: self }
5713    }
5714
5715    #[doc(hidden)]
5716    /// Test-only fault-injection seam. Not part of the supported API; it exists so
5717    /// integration tests in sibling crates can fail a commit at an exact point.
5718    #[doc(hidden)]
5719    #[cfg(any(test, feature = "test-support"))]
5720    pub fn set_before_prepared_span_commit_hook(
5721        &mut self,
5722        hook: impl FnOnce() + Send + Sync + 'static,
5723    ) {
5724        self.before_prepared_span_commit_hook = Some(Box::new(hook));
5725    }
5726
5727    #[doc(hidden)]
5728    /// Test-only fault-injection seam, matching `set_after_eager_proposal_commit_hook`.
5729    #[cfg(test)]
5730    pub(crate) fn set_before_target_preparation_commit_hook(
5731        &mut self,
5732        hook: impl FnOnce() + Send + Sync + 'static,
5733    ) {
5734        self.before_target_preparation_commit_hook = Some(Box::new(hook));
5735    }
5736
5737    fn stage_deferred_formula_package(&mut self, package: crate::engine::DeferredFormulaPackage) {
5738        if let Ok(replay) = package.replay.lock()
5739            && let Some(footprint) = replay.selection_cache_footprint()
5740            && !self
5741                .source_cache_footprints
5742                .iter()
5743                .any(|known| known.ptr_eq(&footprint))
5744        {
5745            self.source_cache_footprints.push(footprint);
5746        }
5747        let sheet = package.sheet_name.clone();
5748        let staged = self.staged_formulas.entry(sheet.clone()).or_default();
5749        debug_assert!(staged.deferred_package.is_none());
5750        staged.deferred_package = Some(package);
5751        staged.reconcile_attached_deferred_package();
5752        self.staged_formula_index
5753            .set_package(&sheet, staged.deferred_package.as_ref());
5754        if let Some(sheet_id) = self.graph.sheet_id(&sheet) {
5755            let package = self
5756                .staged_formulas
5757                .get(&sheet)
5758                .and_then(|staged| staged.deferred_package.as_ref());
5759            for &(vertex, anchor, region) in &self.blocked_pending_spills {
5760                if anchor.sheet_id == sheet_id
5761                    && self.graph.vertex_exists(vertex)
5762                    && self.graph.get_cell_ref(vertex) == Some(anchor)
5763                    && self.staged_formula_index.package_occupies_spill(
5764                        &sheet,
5765                        (anchor.coord.row() + 1, anchor.coord.col() + 1),
5766                        (
5767                            region.rows.query_bounds().1 + 1,
5768                            region.cols.query_bounds().1 + 1,
5769                        ),
5770                        |point| package.is_some_and(|package| package.suppressed.contains(&point)),
5771                    )
5772                {
5773                    self.graph.mark_vertex_dirty(vertex);
5774                }
5775            }
5776        }
5777    }
5778
5779    pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
5780        let mut removed = None;
5781        let mut remove_sheet = false;
5782        let mut had_package = false;
5783        if let Some(entries) = self.staged_formulas.get_mut(sheet) {
5784            had_package = entries.deferred_package.is_some();
5785            removed = entries.remove(row, col);
5786            remove_sheet = entries.is_empty();
5787        }
5788        let ordinary_removed = self.staged_formula_index.remove(sheet, row, col);
5789        if !ordinary_removed && had_package {
5790            self.staged_formula_index.touch_package(sheet);
5791        }
5792        if remove_sheet {
5793            self.staged_formulas.remove(sheet);
5794            self.staged_formula_index.set_package(sheet, None);
5795        }
5796        if (ordinary_removed || had_package)
5797            && let Some(sheet) = self.graph.sheet_id(sheet)
5798        {
5799            self.invalidate_pending_spills(StructuralScope::Cell {
5800                sheet,
5801                row: row.saturating_sub(1),
5802                col: col.saturating_sub(1),
5803            });
5804        }
5805        removed
5806    }
5807
5808    pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
5809        if self.staged_formulas.remove(sheet).is_some() {
5810            self.staged_formula_index.clear_sheet(sheet);
5811            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
5812                self.invalidate_pending_spills(StructuralScope::Sheet(sheet_id));
5813            }
5814        }
5815    }
5816
5817    pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
5818        let Some(entries) = self.staged_formulas.remove(old) else {
5819            return;
5820        };
5821        self.staged_formula_index.clear_sheet(old);
5822        let (formulas, mut package) = entries.into_parts();
5823        for (row, col, text) in formulas {
5824            self.stage_formula_text(new, row, col, text);
5825        }
5826        if let Some(package) = package.as_mut() {
5827            package.sheet_name = new.to_string();
5828        }
5829        if let Some(package) = package {
5830            self.stage_deferred_formula_package(package);
5831        }
5832    }
5833
5834    /// Get a staged formula text for a given cell if present (cloned).
5835    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
5836        self.staged_formulas
5837            .get(sheet)
5838            .and_then(|v| v.get(row, col))
5839    }
5840
5841    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
5842        &self.formula_parse_diagnostics
5843    }
5844
5845    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
5846        std::mem::take(&mut self.formula_parse_diagnostics)
5847    }
5848
5849    pub fn clear_formula_parse_diagnostics(&mut self) {
5850        self.formula_parse_diagnostics.clear();
5851    }
5852
5853    pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
5854        self.last_formula_ingest_report.as_ref()
5855    }
5856
5857    pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
5858        &self.formula_ingest_report_total
5859    }
5860
5861    #[cfg(test)]
5862    pub(crate) fn set_before_target_planning_snapshot_hook_for_test(
5863        &mut self,
5864        hook: impl FnOnce() + Send + Sync + 'static,
5865    ) {
5866        self.before_target_planning_snapshot_hook = Some(Box::new(hook));
5867    }
5868
5869    #[cfg(test)]
5870    pub(crate) fn inject_target_semantic_stale_once_for_test(&mut self) {
5871        self.inject_target_semantic_stale_once_for_test = true;
5872    }
5873
5874    #[cfg(test)]
5875    pub(crate) fn force_virtual_dep_changes_for_test(&mut self, rounds: usize) {
5876        self.force_virtual_dep_changes_remaining_for_test = rounds;
5877    }
5878
5879    #[cfg(test)]
5880    pub(crate) fn fail_evaluation_commit_preflight_once_for_test(&mut self) {
5881        self.fail_evaluation_commit_preflight_once_for_test = true;
5882    }
5883
5884    #[cfg(test)]
5885    pub(crate) fn set_target_preparation_fault_for_test(
5886        &mut self,
5887        fault: crate::engine::target_preparation::TargetPreparationFault,
5888    ) {
5889        self.target_preparation_fault_for_test = Some(fault);
5890    }
5891
5892    #[cfg(test)]
5893    pub(crate) fn staged_formula_index_revision_for_test(&self) -> u64 {
5894        self.staged_formula_index.revision()
5895    }
5896
5897    #[cfg(test)]
5898    pub(crate) fn deferred_package_for_test(
5899        &self,
5900        sheet: &str,
5901    ) -> &crate::engine::DeferredFormulaPackage {
5902        self.staged_formulas
5903            .get(sheet)
5904            .unwrap()
5905            .deferred_package
5906            .as_ref()
5907            .unwrap()
5908    }
5909
5910    #[cfg(test)]
5911    pub(crate) fn staged_formula_index_is_consistent_for_test(&self) -> bool {
5912        let ordinary_storage = self
5913            .staged_formulas
5914            .values()
5915            .map(|sheet| sheet.entries.len())
5916            .sum::<usize>();
5917        let package_storage = self
5918            .staged_formulas
5919            .values()
5920            .filter(|sheet| sheet.deferred_package.is_some())
5921            .count();
5922        ordinary_storage == self.staged_formula_index.ordinary_count()
5923            && package_storage == self.staged_formula_index.package_count()
5924            && self.staged_formulas.iter().all(|(name, sheet)| {
5925                sheet.entries.iter().all(|(row, col, _)| {
5926                    let leases = self
5927                        .staged_formula_index
5928                        .leases_in_region(name, *row, *col, *row, *col);
5929                    leases.len() == 1 && leases[0].row == *row && leases[0].col == *col
5930                })
5931            })
5932    }
5933
5934    #[cfg(test)]
5935    pub(crate) fn evaluation_request_begin_count_for_test(&self) -> u64 {
5936        self.evaluation_request_begin_count_for_test
5937    }
5938
5939    #[cfg(test)]
5940    pub(crate) fn set_before_legacy_fallback_final_provider_sample_hook(
5941        &mut self,
5942        hook: impl FnOnce() + Send + Sync + 'static,
5943    ) {
5944        self.before_legacy_fallback_final_provider_sample_hook = Some(Box::new(hook));
5945    }
5946
5947    #[cfg(test)]
5948    pub(crate) fn set_after_eager_proposal_commit_hook(
5949        &mut self,
5950        hook: impl FnOnce() + Send + Sync + 'static,
5951    ) {
5952        self.after_eager_proposal_commit_hook = Some(Box::new(hook));
5953    }
5954
5955    #[cfg(test)]
5956    pub(crate) fn topology_epoch_for_test(&self) -> u64 {
5957        self.topology_epoch
5958    }
5959
5960    #[cfg(test)]
5961    pub(crate) fn graph_topology_revision_for_test(&self) -> u64 {
5962        self.graph.topology_revision()
5963    }
5964
5965    fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
5966        self.formula_ingest_report_total.mode = report.mode;
5967        self.formula_ingest_report_total.accumulate(&report);
5968        self.last_formula_ingest_report = Some(report);
5969    }
5970
5971    fn collect_planning_function_requests(
5972        ast: &ASTNode,
5973        requests: &mut Vec<(String, String, usize)>,
5974    ) {
5975        match &ast.node_type {
5976            ASTNodeType::Function { name, args } => {
5977                requests.push((String::new(), name.clone(), args.len()));
5978                for arg in args {
5979                    Self::collect_planning_function_requests(arg, requests);
5980                }
5981            }
5982            ASTNodeType::BinaryOp { left, right, .. } => {
5983                Self::collect_planning_function_requests(left, requests);
5984                Self::collect_planning_function_requests(right, requests);
5985            }
5986            ASTNodeType::UnaryOp { expr, .. } => {
5987                Self::collect_planning_function_requests(expr, requests);
5988            }
5989            ASTNodeType::Call { callee, args } => {
5990                Self::collect_planning_function_requests(callee, requests);
5991                for arg in args {
5992                    Self::collect_planning_function_requests(arg, requests);
5993                }
5994            }
5995            ASTNodeType::Array(rows) => {
5996                for cell in rows.iter().flatten() {
5997                    Self::collect_planning_function_requests(cell, requests);
5998                }
5999            }
6000            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {}
6001        }
6002    }
6003
6004    fn prepared_function_semantics_changed(
6005        &self,
6006        preparation: &crate::engine::FormulaCompressedPreparation,
6007        guard: &crate::function_registry::SemanticEpochReadGuard,
6008    ) -> bool {
6009        if preparation.function_semantic_epoch == guard.epoch() {
6010            return false;
6011        }
6012
6013        guard
6014            .semantic_changes_affect_requests_since(preparation.function_semantic_epoch, Vec::new())
6015    }
6016
6017    pub(crate) fn prepare_source_formula_families(
6018        &mut self,
6019        sheet_name: &str,
6020        families: &[crate::engine::SourceFormulaFamily],
6021    ) -> crate::engine::FormulaCompressedPreparation {
6022        crate::engine::FormulaCompressedPreparation {
6023            engine_token: Arc::clone(&self.source_formula_token),
6024            function_semantic_epoch: crate::function_registry::semantic_epoch(),
6025            function_provider_revision: None,
6026            function_semantics_used: false,
6027            sheet_name: Arc::from(sheet_name),
6028            rejected: BTreeMap::new(),
6029            eager_replay: Vec::new(),
6030            preparation_spool_replays: 0,
6031            clean_rejected_anchor_counts: [0; 3],
6032            fragmented_rejected_anchor_counts: [0; 3],
6033            exact_replay: None,
6034            replay_disposition: crate::engine::FormulaReplayDisposition::default(),
6035        }
6036    }
6037
6038    fn prepare_source_formula_proposals(
6039        &mut self,
6040        sheet_name: &str,
6041        families: &[crate::engine::SourceFormulaFamily],
6042        authority_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6043        replay_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6044        formula_record_count: u64,
6045        replay: Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6046        suppressed: &BTreeSet<(u32, u32)>,
6047        consumed: &FxHashSet<(crate::engine::SourceFamilyId, crate::engine::SourceCoord)>,
6048        consumed_engine: Option<&Arc<()>>,
6049    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
6050        let mut preparation = self.prepare_source_formula_families(sheet_name, families);
6051        preparation.exact_replay = Some(Arc::clone(&replay));
6052        preparation
6053            .replay_disposition
6054            .register_consumed_members(consumed_engine, consumed.iter().copied());
6055        if !preparation
6056            .replay_disposition
6057            .consumed_engine_matches(&self.source_formula_token)
6058        {
6059            return Err(ExcelError::new(ExcelErrorKind::Value)
6060                .with_message("ResidualConsumedEngineMismatch"));
6061        }
6062        preparation
6063            .replay_disposition
6064            .extend_suppressed_excel_coords(suppressed.iter().copied());
6065        Ok(preparation)
6066    }
6067
6068    fn formula_batch_from_exact_replay(
6069        &mut self,
6070        sheet_name: &str,
6071        replayed: impl IntoIterator<Item = crate::engine::DeferredReplayFormula>,
6072    ) -> Result<FormulaIngestBatch, ExcelError> {
6073        let mut cache = rustc_hash::FxHashMap::default();
6074        let mut formulas = Vec::new();
6075        for record in replayed {
6076            let key = if record.text.starts_with('=') {
6077                record.text
6078            } else {
6079                format!("={}", record.text)
6080            };
6081            let ast_id = if let Some(cached) = cache.get(&key) {
6082                *cached
6083            } else {
6084                let parsed = match formualizer_parse::parser::parse(&key) {
6085                    Ok(parsed) => parsed,
6086                    Err(error) => {
6087                        let Some(parsed) = self.handle_formula_parse_error(
6088                            sheet_name,
6089                            record.row,
6090                            record.col,
6091                            &key,
6092                            error.to_string(),
6093                        )?
6094                        else {
6095                            continue;
6096                        };
6097                        parsed
6098                    }
6099                };
6100                let ast_id = self.intern_formula_ast(&parsed);
6101                cache.insert(key.clone(), ast_id);
6102                ast_id
6103            };
6104            formulas.push(
6105                FormulaIngestRecord::new(record.row, record.col, ast_id, Some(Arc::from(key)))
6106                    .with_source_proof(record.source_order, record.family, record.partition_owner),
6107            );
6108        }
6109        Ok(FormulaIngestBatch::new(sheet_name.to_string(), formulas))
6110    }
6111
6112    fn prepare_target_combined_legacy_graph(
6113        &self,
6114        packages: &[PreparedTargetSourcePackage],
6115        ordinary: &[PreparedOrdinaryStagedFormula],
6116    ) -> Result<(PreparedLegacyGraphPlan, usize), ExcelError> {
6117        let mut planned_by_coord = BTreeMap::new();
6118        for package in packages {
6119            for (row, col, ast_id, plan) in &package.legacy {
6120                planned_by_coord.insert((package.sheet_id, *row, *col), (*ast_id, plan.clone()));
6121            }
6122        }
6123        for formula in ordinary {
6124            if let Some((ast_id, plan)) = formula.ast_id.zip(formula.plan.clone()) {
6125                planned_by_coord.insert(
6126                    (formula.sheet_id, formula.lease.row, formula.lease.col),
6127                    (ast_id, plan),
6128                );
6129            }
6130        }
6131        let planned = planned_by_coord
6132            .into_iter()
6133            .map(|((sheet_id, row, col), (ast_id, plan))| (sheet_id, row, col, ast_id, plan))
6134            .collect::<Vec<_>>();
6135        let formula_count = planned.len();
6136        let graph = self
6137            .graph
6138            .prepare_legacy_graph_plan_multi_sheet(planned)
6139            .map_err(|error| {
6140                ExcelError::new(ExcelErrorKind::Value)
6141                    .with_message(format!("target graph preparation failed: {error}"))
6142            })?;
6143        Ok((graph, formula_count))
6144    }
6145
6146    fn replay_target_coordinates(
6147        &mut self,
6148        replay: &Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6149        coordinates: &[(u32, u32)],
6150        deadline: Option<std::time::Instant>,
6151        scratch: &mut u64,
6152    ) -> Result<Option<Vec<crate::engine::DeferredReplayFormula>>, ExcelError> {
6153        if coordinates.is_empty() {
6154            return Ok(Some(Vec::new()));
6155        }
6156        let mut guard = replay.lock().map_err(|_| {
6157            ExcelError::new(ExcelErrorKind::Value)
6158                .with_message("deferred formula spool lock poisoned")
6159        })?;
6160        let retained = guard.selection_cache_footprint().is_some();
6161        let records = guard.replay_selected_exact(coordinates, &mut |work, bytes| {
6162            self.target_preparation_checkpoint(deadline, work)?;
6163            if retained && let Some(ledger) = self.active_resource_ledger.as_mut() {
6164                ledger
6165                    .reserve_retained(bytes)
6166                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
6167                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
6168            }
6169            self.reserve_graph_source_scratch(bytes)?;
6170            *scratch = scratch.saturating_add(bytes);
6171            Ok(())
6172        });
6173        drop(guard);
6174        let reconciled = self.reconcile_source_cache_footprints();
6175        let records = records?;
6176        reconciled?;
6177        Ok(records)
6178    }
6179
6180    fn prepare_target_exact_source_selection(
6181        &mut self,
6182        sheet: &str,
6183        lease: StagedPackageLease,
6184        coordinates: Vec<(u32, u32)>,
6185        previous: &BTreeSet<(u32, u32)>,
6186        allow_partial_shared: bool,
6187        deadline: Option<std::time::Instant>,
6188        scratch: &mut u64,
6189    ) -> Result<Option<PreparedTargetSourcePackage>, ExcelError> {
6190        let package = self
6191            .staged_formulas
6192            .get(sheet)
6193            .and_then(|s| s.deferred_package.as_ref())
6194            .unwrap();
6195        if !package.source_geometry_complete
6196            || ((!package.families.is_empty() || !package.partitioned_families.is_empty())
6197                && !package.coordinates_cover_families)
6198            || package.reconciliation_replay.is_some()
6199        {
6200            return Ok(None);
6201        }
6202        let selected_points: BTreeSet<_> = coordinates
6203            .into_iter()
6204            .filter(|point| !package.suppressed.contains(point))
6205            .collect();
6206        let mut prepared = PreparedTargetSourcePackage::empty_selection(
6207            sheet,
6208            self.graph.sheet_id(sheet).unwrap(),
6209            lease,
6210        );
6211        if selected_points.is_empty() {
6212            return Ok(Some(prepared));
6213        }
6214        let mut routing = crate::engine::FormulaReplayDisposition::default();
6215        for partition in &package.partitioned_families {
6216            routing
6217                .register_partition(partition, false)
6218                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?;
6219        }
6220        routing.extend_suppressed_excel_coords(package.suppressed.iter().copied());
6221        routing.register_consumed_members(
6222            package.consumed_engine.as_ref(),
6223            package.consumed_members.iter().copied(),
6224        );
6225        let replay = Arc::clone(&package.replay);
6226        let source_report = package.accounting_report();
6227        prepared.source_report = source_report;
6228        prepared.disposition = routing.clone();
6229
6230        let replay_points: BTreeSet<_> = selected_points
6231            .iter()
6232            .copied()
6233            .filter(|&(row, col)| !prepared.direct_contains(row, col))
6234            .collect();
6235        let coordinates: Vec<_> = replay_points.iter().copied().collect();
6236        let records = self.replay_target_coordinates(&replay, &coordinates, deadline, scratch)?;
6237        prepared.spool_replays = u64::from(!coordinates.is_empty());
6238        let Some(mut replay_records) = records else {
6239            return Ok(None);
6240        };
6241        for record in &mut replay_records {
6242            if record.family.is_none() {
6243                record.partition_owner = routing
6244                    .ordinary_disposition(crate::engine::SourceCoord {
6245                        row: record.row.saturating_sub(1),
6246                        col: record.col.saturating_sub(1),
6247                    })
6248                    .1;
6249            }
6250        }
6251        replay_records.sort_by_key(|record| record.source_order);
6252        if replay_records
6253            .iter()
6254            .any(|record| !selected_points.contains(&(record.row, record.col)))
6255            || replay_records
6256                .windows(2)
6257                .any(|records| records[0].source_order == records[1].source_order)
6258        {
6259            return Err(ExcelError::new(ExcelErrorKind::Value)
6260                .with_message("invalid indexed exact source selection"));
6261        }
6262        let represented: BTreeSet<_> = replay_records.iter().map(|r| (r.row, r.col)).collect();
6263        if represented != replay_points {
6264            return Err(ExcelError::new(ExcelErrorKind::Value)
6265                .with_message("incomplete indexed exact source selection"));
6266        }
6267        // The last source record must agree with compressed ownership evidence.
6268        // Earlier overridden records retain ordering but demand no dependencies.
6269        let source = self
6270            .staged_formulas
6271            .get(sheet)
6272            .unwrap()
6273            .deferred_package
6274            .as_ref()
6275            .unwrap();
6276        let contains = |rect: crate::engine::SourceRect, coord: crate::engine::SourceCoord| {
6277            coord.row >= rect.start.row
6278                && coord.row <= rect.end.row
6279                && coord.col >= rect.start.col
6280                && coord.col <= rect.end.col
6281        };
6282        let mut checked = BTreeSet::new();
6283        for record in replay_records.iter().rev() {
6284            if !checked.insert((record.row, record.col)) {
6285                continue;
6286            }
6287            let coord = crate::engine::SourceCoord {
6288                row: record.row - 1,
6289                col: record.col - 1,
6290            };
6291            let agrees = |owner, shared: bool| {
6292                record.family == shared.then_some(owner)
6293                    && record.partition_owner.or(record.family) == Some(owner)
6294            };
6295            let mut valid = true;
6296            for family in &source.families {
6297                let owns = match &family.members {
6298                    crate::engine::SourceFamilyMembers::CompleteDomain(domain) => {
6299                        contains(domain.rect(), coord)
6300                    }
6301                    crate::engine::SourceFamilyMembers::ExplicitMembers(members) => {
6302                        members.as_slice().binary_search(&coord).is_ok()
6303                    }
6304                };
6305                if owns && !agrees(family.source_id, true) {
6306                    valid = false;
6307                }
6308            }
6309            for family in &source.partitioned_families {
6310                if family
6311                    .fragments
6312                    .iter()
6313                    .any(|fragment| contains(fragment.rect(), coord))
6314                    && !agrees(family.source_id, true)
6315                {
6316                    valid = false;
6317                }
6318                if let Some(member) = family
6319                    .legacy_members
6320                    .as_slice()
6321                    .iter()
6322                    .find(|member| member.coord == coord)
6323                    && !agrees(
6324                        family.source_id,
6325                        member.kind == crate::engine::PartitionLegacyMemberKind::SharedFamilyMember,
6326                    )
6327                {
6328                    valid = false;
6329                }
6330            }
6331            if !valid {
6332                return Err(ExcelError::new(ExcelErrorKind::Value)
6333                    .with_message("indexed source ownership mismatch"));
6334            }
6335        }
6336        prepared.selected_points = Some(selected_points);
6337        prepared.replay_records = replay_records;
6338        Ok(Some(prepared))
6339    }
6340
6341    fn prepare_target_source_package(
6342        &mut self,
6343        sheet: &str,
6344        lease: StagedPackageLease,
6345        deadline: Option<std::time::Instant>,
6346    ) -> Result<PreparedTargetSourcePackage, ExcelError> {
6347        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
6348            ExcelError::new(ExcelErrorKind::Ref)
6349                .with_message(format!("deferred source sheet not found: {sheet}"))
6350        })?;
6351        let (
6352            source_report,
6353            families,
6354            partitions,
6355            replay,
6356            invalidated,
6357            suppressed,
6358            consumed_members,
6359            consumed_engine,
6360            reconciliation_replay,
6361        ) = {
6362            let package = self
6363                .staged_formulas
6364                .get(sheet)
6365                .and_then(|staged| staged.deferred_package.as_ref())
6366                .ok_or_else(|| {
6367                    ExcelError::new(ExcelErrorKind::Value)
6368                        .with_message("staged deferred source package is unavailable")
6369                })?;
6370            if package.sheet_name != sheet {
6371                return Err(ExcelError::new(ExcelErrorKind::Value)
6372                    .with_message("deferred formula package sheet mismatch"));
6373            }
6374            (
6375                package.accounting_report(),
6376                package.families.clone(),
6377                package.partitioned_families.clone(),
6378                Arc::clone(&package.replay),
6379                package.invalidated.clone(),
6380                package.suppressed.clone(),
6381                package.consumed_members.clone(),
6382                package.consumed_engine.clone(),
6383                package.reconciliation_replay.clone(),
6384            )
6385        };
6386
6387        let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
6388        for partition in &partitions {
6389            replay_disposition
6390                .register_partition(partition, false)
6391                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
6392        }
6393        replay_disposition.extend_suppressed_excel_coords(suppressed.iter().copied());
6394        replay_disposition
6395            .register_consumed_members(consumed_engine.as_ref(), consumed_members.iter().copied());
6396        if !replay_disposition.consumed_engine_matches(&self.source_formula_token) {
6397            return Err(ExcelError::new(ExcelErrorKind::Value)
6398                .with_message("ResidualConsumedEngineMismatch"));
6399        }
6400        self.target_preparation_checkpoint(deadline, 1)?;
6401        let mut replay_records = if let Some(mut records) = reconciliation_replay {
6402            records.retain(|record| {
6403                let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1))
6404                else {
6405                    return true;
6406                };
6407                let coord = crate::engine::SourceCoord { row, col };
6408                let disposition = record.family.map_or_else(
6409                    || replay_disposition.ordinary_disposition(coord).0,
6410                    |family| replay_disposition.shared_disposition(family, coord),
6411                );
6412                !matches!(
6413                    disposition,
6414                    crate::engine::FormulaReplayCoordinateDisposition::Direct
6415                        | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
6416                )
6417            });
6418            records
6419        } else {
6420            replay
6421                .lock()
6422                .map_err(|_| {
6423                    ExcelError::new(ExcelErrorKind::Value)
6424                        .with_message("deferred formula spool lock poisoned")
6425                })?
6426                .replay_partitioned(&replay_disposition, &partitions)
6427                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?
6428        };
6429        replay_records.sort_by_key(|record| record.source_order);
6430        for chunk in replay_records.chunks(256) {
6431            self.target_preparation_checkpoint(deadline, chunk.len() as u64)?;
6432        }
6433        if replay_records
6434            .windows(2)
6435            .any(|records| records[0].source_order == records[1].source_order)
6436        {
6437            return Err(ExcelError::new(ExcelErrorKind::Value)
6438                .with_message("duplicate deferred source-order proof"));
6439        }
6440
6441        let disposition = replay_disposition;
6442        let direct_families = 0usize;
6443        let direct_cells = 0u64;
6444        let direct_fragments = 0u64;
6445        let direct_complete_families = 0u64;
6446        let direct_complete_cells = 0u64;
6447        let direct_partition_families = 0u64;
6448        let direct_partition_cells = 0u64;
6449        let anchor_parses = 0u64;
6450        let anchor_asts = 0u64;
6451        let anchor_analyses = 0u64;
6452
6453        Ok(PreparedTargetSourcePackage {
6454            sheet: sheet.to_string(),
6455            sheet_id,
6456            lease,
6457            selected_points: None,
6458            complete_selections: Default::default(),
6459            deferred_shared: false,
6460            direct_domains: Vec::new(),
6461            source_report,
6462            replay_records,
6463            spool_replays: 1,
6464            disposition,
6465            legacy: Vec::new(),
6466            direct_families,
6467            direct_cells,
6468            direct_fragments,
6469            direct_complete_families,
6470            direct_complete_cells,
6471            direct_partition_families,
6472            direct_partition_cells,
6473            anchor_parses,
6474            anchor_asts,
6475            anchor_analyses,
6476        })
6477    }
6478
6479    fn fallback_planning_snapshot(
6480        &self,
6481        batch: &FormulaIngestBatch,
6482    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
6483        let mut requests = Vec::new();
6484        for formula in &batch.formulas {
6485            let ast = self
6486                .graph
6487                .data_store()
6488                .retrieve_ast(formula.ast_id, self.graph.sheet_reg())
6489                .ok_or_else(|| {
6490                    ExcelError::new(ExcelErrorKind::Value)
6491                        .with_message("ordered fallback AST is unavailable")
6492                })?;
6493            Self::collect_planning_function_requests(&ast, &mut requests);
6494        }
6495        requests.sort();
6496        requests.dedup();
6497        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
6498            &self.resolver,
6499            requests,
6500        )
6501        .map_err(|error| ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}")))
6502    }
6503
6504    fn prepare_legacy_batch_fallback(
6505        &mut self,
6506        batch: FormulaIngestBatch,
6507        function_provider: &dyn crate::traits::FunctionProvider,
6508    ) -> Result<
6509        (
6510            crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan,
6511            u64,
6512        ),
6513        ExcelError,
6514    > {
6515        let formula_count = batch.formulas.len() as u64;
6516        let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
6517        let mut planned = Vec::with_capacity(batch.formulas.len());
6518        for record in batch.formulas {
6519            let placement = CellRef::new(
6520                sheet_id,
6521                Coord::from_excel(record.row, record.col, true, true),
6522            );
6523            let ingested = self
6524                .graph
6525                .ingest_pipeline(function_provider)
6526                .enable_function_semantics()
6527                .ingest_formula(
6528                    FormulaAstInput::RawArena(record.ast_id),
6529                    placement,
6530                    record.formula_text,
6531                )
6532                .map_err(|error| {
6533                    ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}"))
6534                })?;
6535            planned.push((record.row, record.col, ingested.ast_id, ingested.dep_plan));
6536        }
6537        let plan = self
6538            .graph
6539            .prepare_legacy_graph_plan(sheet_id, planned)
6540            .map_err(|error| {
6541                ExcelError::new(ExcelErrorKind::Value).with_message(error.to_string())
6542            })?;
6543        Ok((plan, formula_count))
6544    }
6545
6546    fn publish_compressed_partial_report(
6547        &mut self,
6548        report: &FormulaIngestReport,
6549        direct_report: &FormulaIngestReport,
6550    ) {
6551        if direct_report.source_family_promoted == 0
6552            && direct_report.graph_formula_cells_materialized == 0
6553        {
6554            return;
6555        }
6556        let mut published = report.clone();
6557        published.accumulate(direct_report);
6558        self.record_formula_ingest_report(published);
6559    }
6560
6561    fn finish_compressed_formula_sources(
6562        &mut self,
6563        batches: Vec<(
6564            FormulaIngestBatch,
6565            crate::engine::FormulaCompressedSourceReport,
6566            crate::engine::FormulaCompressedPreparation,
6567        )>,
6568    ) -> Result<FormulaIngestReport, ExcelError> {
6569        self.observe_function_semantic_epoch()?;
6570        if batches.iter().any(|(_, _, preparation)| {
6571            !Arc::ptr_eq(&preparation.engine_token, &self.source_formula_token)
6572        }) {
6573            return Err(ExcelError::new(ExcelErrorKind::Value)
6574                .with_message("compressed source preparation belongs to another engine"));
6575        }
6576        if batches.iter().any(|(fallback, _, preparation)| {
6577            preparation.sheet_name.as_ref() != fallback.sheet_name
6578        }) {
6579            return Err(ExcelError::new(ExcelErrorKind::Value)
6580                .with_message("compressed source preparation sheet mismatch"));
6581        }
6582        let initial_guard = crate::function_registry::semantic_epoch_read_guard();
6583        let initial_provider_revision = self.resolver.planning_semantic_revision();
6584        let mut fallback_batches = Vec::with_capacity(batches.len());
6585        let mut stale_fallback_batches = Vec::new();
6586        let mut pending_preparations = Vec::new();
6587        for (mut fallback, mut source, mut preparation) in batches {
6588            for formula in fallback.formulas.drain(..) {
6589                let source_order = formula.source_order.ok_or_else(|| {
6590                    ExcelError::new(ExcelErrorKind::Value).with_message(
6591                        "compressed source supplied formulas without source-order proof",
6592                    )
6593                })?;
6594                let text = formula.formula_text.ok_or_else(|| {
6595                    ExcelError::new(ExcelErrorKind::Value).with_message(
6596                        "ordered compressed fallback formula has no exact source text",
6597                    )
6598                })?;
6599                preparation
6600                    .eager_replay
6601                    .push(crate::engine::DeferredReplayFormula {
6602                        source_order,
6603                        row: formula.row,
6604                        col: formula.col,
6605                        text: text.to_string(),
6606                        family: formula.source_family,
6607                        partition_owner: formula.partition_owner,
6608                    });
6609            }
6610            preparation
6611                .eager_replay
6612                .sort_by_key(|record| record.source_order);
6613            source.source_spool_replays = source
6614                .source_spool_replays
6615                .saturating_add(preparation.preparation_spool_replays);
6616            let stale_reason = preparation
6617                .function_semantics_used
6618                .then(|| {
6619                    if preparation.function_provider_revision != initial_provider_revision {
6620                        Some("FunctionProviderRevisionChanged")
6621                    } else if self.prepared_function_semantics_changed(&preparation, &initial_guard)
6622                    {
6623                        Some("FunctionSemanticEpochChanged")
6624                    } else {
6625                        None
6626                    }
6627                })
6628                .flatten();
6629            let stale_semantics = stale_reason.is_some();
6630            for reason in preparation.rejected.values() {
6631                *source.fallback_reasons.entry(reason.clone()).or_default() += 1;
6632            }
6633            // No family is ever placed directly: every family replays.
6634            source.replay_families = source.families_seen;
6635            source.replay_cells = source.family_cells_seen;
6636            let compressed = crate::engine::FormulaCompressedSourceBatch::new(
6637                fallback.sheet_name.clone(),
6638                source,
6639            );
6640            if stale_semantics {
6641                stale_fallback_batches.push((fallback, compressed));
6642            } else {
6643                fallback_batches.push((fallback, compressed));
6644            }
6645            pending_preparations.push((preparation, stale_semantics));
6646        }
6647        drop(initial_guard);
6648
6649        // Build known fallback graphs first. Stale batches are forced through legacy ingest.
6650        let configured_mode = self.config.formula_plane_mode;
6651        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6652        let stale_result =
6653            self.ingest_compressed_formula_source_batches_inner(stale_fallback_batches, false);
6654        self.config.formula_plane_mode = configured_mode;
6655        let mut report = stale_result?;
6656        report.mode = configured_mode;
6657
6658        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6659        let fallback_result =
6660            self.ingest_compressed_formula_source_batches_inner(fallback_batches, false);
6661        self.config.formula_plane_mode = configured_mode;
6662        match fallback_result {
6663            Ok(fallback_report) => report.accumulate(&fallback_report),
6664            Err(error) => {
6665                self.record_formula_ingest_report(report);
6666                return Err(error);
6667            }
6668        }
6669
6670        let mut direct_report =
6671            FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
6672        for (preparation, _) in &pending_preparations {
6673            direct_report.source_anchor_parses = direct_report.source_anchor_parses.saturating_add(
6674                preparation.clean_rejected_anchor_counts[0]
6675                    .saturating_add(preparation.fragmented_rejected_anchor_counts[0]),
6676            );
6677            direct_report.source_anchor_asts = direct_report.source_anchor_asts.saturating_add(
6678                preparation.clean_rejected_anchor_counts[1]
6679                    .saturating_add(preparation.fragmented_rejected_anchor_counts[1]),
6680            );
6681            direct_report.source_anchor_analyses =
6682                direct_report.source_anchor_analyses.saturating_add(
6683                    preparation.clean_rejected_anchor_counts[2]
6684                        .saturating_add(preparation.fragmented_rejected_anchor_counts[2]),
6685                );
6686        }
6687        loop {
6688            #[cfg(any(test, feature = "test-support"))]
6689            if let Some(hook) = self.before_prepared_span_commit_hook.take() {
6690                hook();
6691            }
6692            let commit_guard = crate::function_registry::semantic_epoch_read_guard();
6693            let commit_provider_revision = self.resolver.planning_semantic_revision();
6694            let mut newly_stale = Vec::new();
6695            let mut current = Vec::new();
6696            for pending in pending_preparations.drain(..) {
6697                let stale_reason = pending
6698                    .0
6699                    .function_semantics_used
6700                    .then(|| {
6701                        if pending.0.function_provider_revision != commit_provider_revision {
6702                            Some("FunctionProviderRevisionChanged")
6703                        } else if self
6704                            .prepared_function_semantics_changed(&pending.0, &commit_guard)
6705                        {
6706                            Some("FunctionSemanticEpochChanged")
6707                        } else {
6708                            None
6709                        }
6710                    })
6711                    .flatten();
6712                if let Some(reason) = stale_reason {
6713                    newly_stale.push((pending, reason));
6714                } else {
6715                    current.push(pending);
6716                }
6717            }
6718            if !newly_stale.is_empty() {
6719                drop(commit_guard);
6720                for ((mut preparation, was_initially_stale), reason) in newly_stale {
6721                    preparation
6722                        .eager_replay
6723                        .sort_by_key(|record| record.source_order);
6724                    direct_report.source_spool_replays =
6725                        direct_report.source_spool_replays.saturating_add(1);
6726                    if !was_initially_stale {
6727                        direct_report
6728                            .fallback_reasons
6729                            .entry(reason.to_string())
6730                            .or_default();
6731                    }
6732                    preparation.function_semantics_used = false;
6733                    if !preparation.eager_replay.is_empty() {
6734                        current.push((preparation, false));
6735                    }
6736                }
6737                pending_preparations = current;
6738                continue;
6739            }
6740            drop(commit_guard);
6741
6742            // Replay fallback families in source order.
6743            enum SourceProposal {
6744                KnownFallback {
6745                    source_order: crate::engine::SourceFormulaOrder,
6746                    records: Vec<crate::engine::DeferredReplayFormula>,
6747                },
6748            }
6749
6750            impl SourceProposal {
6751                fn source_order(&self) -> crate::engine::SourceFormulaOrder {
6752                    match self {
6753                        Self::KnownFallback { source_order, .. } => *source_order,
6754                    }
6755                }
6756            }
6757
6758            for (mut preparation, _) in current {
6759                let mut proposals = Vec::with_capacity(preparation.eager_replay.len());
6760                let mut family_fallbacks: BTreeMap<_, Vec<_>> = BTreeMap::new();
6761                for record in preparation.eager_replay.drain(..) {
6762                    if let Some(owner) = record.partition_owner.or(record.family) {
6763                        family_fallbacks.entry(owner).or_default().push(record);
6764                    } else {
6765                        proposals.push(SourceProposal::KnownFallback {
6766                            source_order: record.source_order,
6767                            records: vec![record],
6768                        });
6769                    }
6770                }
6771                for (_, mut records) in family_fallbacks {
6772                    records.sort_by_key(|record| record.source_order);
6773                    if records
6774                        .windows(2)
6775                        .any(|window| window[0].source_order == window[1].source_order)
6776                    {
6777                        self.publish_compressed_partial_report(&report, &direct_report);
6778                        return Err(ExcelError::new(ExcelErrorKind::Value)
6779                            .with_message("duplicate exact-replay source-order proof"));
6780                    }
6781                    let Some(source_order) = records.first().map(|record| record.source_order)
6782                    else {
6783                        self.publish_compressed_partial_report(&report, &direct_report);
6784                        return Err(ExcelError::new(ExcelErrorKind::Value)
6785                            .with_message("empty exact-replay fallback family"));
6786                    };
6787                    proposals.push(SourceProposal::KnownFallback {
6788                        source_order,
6789                        records,
6790                    });
6791                }
6792                proposals.sort_by_key(SourceProposal::source_order);
6793                if proposals
6794                    .windows(2)
6795                    .any(|window| window[0].source_order() == window[1].source_order())
6796                {
6797                    self.publish_compressed_partial_report(&report, &direct_report);
6798                    return Err(ExcelError::new(ExcelErrorKind::Value)
6799                        .with_message("ambiguous compressed source-order proof"));
6800                }
6801
6802                for proposal in proposals {
6803                    match proposal {
6804                        SourceProposal::KnownFallback { records, .. } => {
6805                            let batch = match self.formula_batch_from_exact_replay(
6806                                preparation.sheet_name.as_ref(),
6807                                records,
6808                            ) {
6809                                Ok(batch) => batch,
6810                                Err(error) => {
6811                                    self.publish_compressed_partial_report(&report, &direct_report);
6812                                    return Err(error);
6813                                }
6814                            };
6815                            if batch.is_empty() {
6816                                continue;
6817                            }
6818                            let snapshot = match self.fallback_planning_snapshot(&batch) {
6819                                Ok(snapshot) => snapshot,
6820                                Err(error) => {
6821                                    self.publish_compressed_partial_report(&report, &direct_report);
6822                                    return Err(error);
6823                                }
6824                            };
6825                            let commit_guard =
6826                                crate::function_registry::semantic_epoch_read_guard();
6827                            let provider_revision_initial =
6828                                self.resolver.planning_semantic_revision();
6829                            if (commit_guard.epoch() != snapshot.epoch()
6830                                && snapshot.semantic_changes_affect_requests_since_guarded(
6831                                    &commit_guard,
6832                                    snapshot.epoch(),
6833                                ))
6834                                || snapshot.provider_revision().is_some_and(|revision| {
6835                                    Some(revision) != provider_revision_initial
6836                                })
6837                            {
6838                                drop(commit_guard);
6839                                self.publish_compressed_partial_report(&report, &direct_report);
6840                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6841                                    "ordered fallback planning snapshot became stale",
6842                                ));
6843                            }
6844                            let (plan, formula_count) = match self
6845                                .prepare_legacy_batch_fallback(batch, &snapshot)
6846                            {
6847                                Ok(plan) => plan,
6848                                Err(error) => {
6849                                    drop(commit_guard);
6850                                    self.publish_compressed_partial_report(&report, &direct_report);
6851                                    return Err(error);
6852                                }
6853                            };
6854                            let provider_revision_after =
6855                                self.resolver.planning_semantic_revision();
6856                            if provider_revision_after != provider_revision_initial {
6857                                drop(commit_guard);
6858                                self.publish_compressed_partial_report(&report, &direct_report);
6859                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6860                                    "function provider changed while preparing ordered fallback",
6861                                ));
6862                            }
6863                            let graph_vertices = plan.new_vertex_count();
6864                            let Some(graph_edges) = plan.planned_edge_count() else {
6865                                drop(commit_guard);
6866                                self.publish_compressed_partial_report(&report, &direct_report);
6867                                return Err(ExcelError::new(ExcelErrorKind::Value)
6868                                    .with_message("prepared ordered fallback size overflow"));
6869                            };
6870                            if let Err(error) = self.prepared_legacy_admission(&plan, formula_count)
6871                            {
6872                                drop(commit_guard);
6873                                self.publish_compressed_partial_report(&report, &direct_report);
6874                                return Err(error);
6875                            }
6876                            if let Err(error) =
6877                                self.graph.validate_prepared_legacy_graph_plan(&plan)
6878                            {
6879                                drop(commit_guard);
6880                                self.publish_compressed_partial_report(&report, &direct_report);
6881                                return Err(ExcelError::new(ExcelErrorKind::Value)
6882                                    .with_message(error.to_string()));
6883                            }
6884                            #[cfg(test)]
6885                            if let Some(hook) = self
6886                                .before_legacy_fallback_final_provider_sample_hook
6887                                .take()
6888                            {
6889                                hook();
6890                            }
6891                            let provider_revision_final =
6892                                self.resolver.planning_semantic_revision();
6893                            if provider_revision_final != provider_revision_initial {
6894                                drop(commit_guard);
6895                                self.publish_compressed_partial_report(&report, &direct_report);
6896                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6897                                    "function provider changed after ordered fallback validation",
6898                                ));
6899                            }
6900                            let graph_formulas =
6901                                self.graph.apply_prevalidated_legacy_graph_plan(plan);
6902                            direct_report.formula_cells_seen = direct_report
6903                                .formula_cells_seen
6904                                .saturating_add(formula_count);
6905                            direct_report.graph_formula_cells_materialized = direct_report
6906                                .graph_formula_cells_materialized
6907                                .saturating_add(graph_formulas as u64);
6908                            direct_report.graph_vertices_created = direct_report
6909                                .graph_vertices_created
6910                                .saturating_add(graph_vertices as u64);
6911                            direct_report.graph_edges_created = direct_report
6912                                .graph_edges_created
6913                                .saturating_add(graph_edges as u64);
6914                        }
6915                    }
6916                    #[cfg(test)]
6917                    if let Some(hook) = self.after_eager_proposal_commit_hook.take() {
6918                        hook();
6919                    }
6920                }
6921            }
6922            break;
6923        }
6924        report.accumulate(&direct_report);
6925        self.record_formula_ingest_report(report.clone());
6926        Ok(report)
6927    }
6928    /// Ingest replayed per-cell formulas while preserving compressed source counters.
6929    pub(crate) fn ingest_compressed_formula_source_batches(
6930        &mut self,
6931        batches: Vec<(
6932            FormulaIngestBatch,
6933            crate::engine::FormulaCompressedSourceBatch,
6934        )>,
6935    ) -> Result<FormulaIngestReport, ExcelError> {
6936        self.ingest_compressed_formula_source_batches_inner(batches, true)
6937    }
6938
6939    fn ingest_compressed_formula_source_batches_inner(
6940        &mut self,
6941        batches: Vec<(
6942            FormulaIngestBatch,
6943            crate::engine::FormulaCompressedSourceBatch,
6944        )>,
6945        publish_report: bool,
6946    ) -> Result<FormulaIngestReport, ExcelError> {
6947        let mut source_counts = [0_u64; 11];
6948        let mut source_report = crate::engine::FormulaCompressedSourceReport::default();
6949        let mut formula_batches = Vec::with_capacity(batches.len());
6950        let mut compressed_families = Vec::new();
6951        let mut partitioned_families = Vec::new();
6952        for (batch, compressed_batch) in batches {
6953            let (sheet_name, compressed, families, partitions) = compressed_batch.into_parts();
6954            if sheet_name.as_ref() != batch.sheet_name {
6955                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
6956                    "compressed formula source sheet does not match its replay batch",
6957                ));
6958            }
6959            compressed_families.push((batch.sheet_name.clone(), families));
6960            partitioned_families.push((batch.sheet_name.clone(), partitions));
6961            source_counts[0] = source_counts[0].saturating_add(compressed.source_formula_events);
6962            source_counts[1] = source_counts[1].saturating_add(compressed.source_ordinary_events);
6963            source_counts[2] =
6964                source_counts[2].saturating_add(compressed.source_shared_anchor_events);
6965            source_counts[3] =
6966                source_counts[3].saturating_add(compressed.source_shared_descendant_events);
6967            source_counts[4] = source_counts[4].saturating_add(compressed.source_unknown_events);
6968            source_counts[5] =
6969                source_counts[5].saturating_add(compressed.source_formula_records_spooled);
6970            source_counts[6] =
6971                source_counts[6].saturating_add(compressed.source_spool_encoded_bytes);
6972            source_counts[7] = source_counts[7].max(compressed.source_spool_peak_memory_bytes);
6973            source_counts[8] =
6974                source_counts[8].saturating_add(compressed.source_spool_spilled_bytes);
6975            source_counts[9] = source_counts[9].saturating_add(compressed.source_spool_spill_files);
6976            source_counts[10] = source_counts[10].saturating_add(compressed.source_spool_replays);
6977            source_report.families_seen = source_report
6978                .families_seen
6979                .saturating_add(compressed.families_seen);
6980            source_report.family_cells_seen = source_report
6981                .family_cells_seen
6982                .saturating_add(compressed.family_cells_seen);
6983            source_report.source_clean_families = source_report
6984                .source_clean_families
6985                .saturating_add(compressed.source_clean_families);
6986            source_report.source_clean_cells = source_report
6987                .source_clean_cells
6988                .saturating_add(compressed.source_clean_cells);
6989            source_report.source_fragmentable_families = source_report
6990                .source_fragmentable_families
6991                .saturating_add(compressed.source_fragmentable_families);
6992            source_report.source_fragmentable_cells = source_report
6993                .source_fragmentable_cells
6994                .saturating_add(compressed.source_fragmentable_cells);
6995            source_report.source_fragment_count = source_report
6996                .source_fragment_count
6997                .saturating_add(compressed.source_fragment_count);
6998            source_report.source_isolated_fallback_cells = source_report
6999                .source_isolated_fallback_cells
7000                .saturating_add(compressed.source_isolated_fallback_cells);
7001            source_report.source_hole_exclusions = source_report
7002                .source_hole_exclusions
7003                .saturating_add(compressed.source_hole_exclusions);
7004            source_report.source_ordinary_exclusions = source_report
7005                .source_ordinary_exclusions
7006                .saturating_add(compressed.source_ordinary_exclusions);
7007            source_report.source_partition_failures = source_report
7008                .source_partition_failures
7009                .saturating_add(compressed.source_partition_failures);
7010            source_report.replay_families = source_report
7011                .replay_families
7012                .saturating_add(compressed.replay_families);
7013            source_report.replay_cells = source_report
7014                .replay_cells
7015                .saturating_add(compressed.replay_cells);
7016            source_report.forward_descendants = source_report
7017                .forward_descendants
7018                .saturating_add(compressed.forward_descendants);
7019            source_report.evidence_limit_fallbacks = source_report
7020                .evidence_limit_fallbacks
7021                .saturating_add(compressed.evidence_limit_fallbacks);
7022            source_report.evidence_peak_bytes = source_report
7023                .evidence_peak_bytes
7024                .max(compressed.evidence_peak_bytes);
7025            for (reason, count) in compressed.fallback_reasons {
7026                *source_report.fallback_reasons.entry(reason).or_default() += count;
7027            }
7028            formula_batches.push(batch);
7029        }
7030        self.ingest_formula_batches_inner(
7031            formula_batches,
7032            source_counts,
7033            Some(source_report),
7034            compressed_families,
7035            partitioned_families,
7036            publish_report,
7037        )
7038    }
7039
7040    pub fn ingest_formula_batches(
7041        &mut self,
7042        batches: Vec<FormulaIngestBatch>,
7043    ) -> Result<FormulaIngestReport, ExcelError> {
7044        let has_formulas = batches.iter().any(|batch| !batch.formulas.is_empty());
7045        let report = self.ingest_formula_batches_inner(
7046            batches,
7047            [0; 11],
7048            None,
7049            Vec::new(),
7050            Vec::new(),
7051            true,
7052        )?;
7053        if has_formulas {
7054            self.mark_topology_edited();
7055        }
7056        Ok(report)
7057    }
7058
7059    fn ingest_formula_batches_unpublished(
7060        &mut self,
7061        batches: Vec<FormulaIngestBatch>,
7062    ) -> Result<FormulaIngestReport, ExcelError> {
7063        self.ingest_formula_batches_inner(batches, [0; 11], None, Vec::new(), Vec::new(), false)
7064    }
7065
7066    fn ingest_formula_batches_inner(
7067        &mut self,
7068        batches: Vec<FormulaIngestBatch>,
7069        source_counts: [u64; 11],
7070        source_report: Option<crate::engine::FormulaCompressedSourceReport>,
7071        compressed_families: Vec<(String, Vec<crate::engine::SourceFormulaFamily>)>,
7072        partitioned_families: Vec<(String, Vec<crate::engine::PartitionedSourceFormulaFamily>)>,
7073        publish_report: bool,
7074    ) -> Result<FormulaIngestReport, ExcelError> {
7075        self.observe_function_semantic_epoch()?;
7076        let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
7077        #[cfg(feature = "tracing")]
7078        let arena_nodes_before = self.graph.data_store().memory_usage().total_ast_nodes;
7079        #[cfg(feature = "tracing")]
7080        let route = if !partitioned_families.is_empty() {
7081            "partitioned"
7082        } else if !compressed_families.is_empty() {
7083            "compressed_source"
7084        } else if source_report.is_some() {
7085            "replay"
7086        } else {
7087            "ordinary"
7088        };
7089        let _ingest_span = crate::engine::trace::fz_span!(
7090            tracing::Level::INFO,
7091            "ingest",
7092            "ingest.batch",
7093            mode = ?FormulaPlaneMode::Off,
7094            route,
7095            formula_cells = formula_cells_seen
7096        );
7097        let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
7098        let materialize_batches = batches;
7099        report.formula_cells_seen = formula_cells_seen;
7100        report.source_formula_events = source_counts[0];
7101        report.source_ordinary_events = source_counts[1];
7102        report.source_shared_anchor_events = source_counts[2];
7103        report.source_shared_descendant_events = source_counts[3];
7104        report.source_unknown_events = source_counts[4];
7105        report.source_formula_records_spooled = source_counts[5];
7106        report.source_spool_encoded_bytes = source_counts[6];
7107        report.source_spool_peak_memory_bytes = source_counts[7];
7108        report.source_spool_spilled_bytes = source_counts[8];
7109        report.source_spool_spill_files = source_counts[9];
7110        report.source_spool_replays = source_counts[10];
7111        if let Some(source) = source_report {
7112            report.source_families_seen = source.families_seen;
7113            report.source_family_cells_seen = source.family_cells_seen;
7114            report.source_family_shadow_eligible = source.source_clean_families;
7115            report.source_family_shadow_eligible_cells = source.source_clean_cells;
7116            report.source_partitioned_families_seen = source.source_fragmentable_families;
7117            report.source_partition_holes = source.source_hole_exclusions;
7118            report.source_partition_ordinary_exceptions = source.source_ordinary_exclusions;
7119            report.source_partition_failures = source.source_partition_failures;
7120            report.source_partition_surviving_cells = source.source_fragmentable_cells;
7121            report.source_family_fallback = report
7122                .source_family_fallback
7123                .saturating_add(source.replay_families);
7124            report.source_family_fallback_cells = report
7125                .source_family_fallback_cells
7126                .saturating_add(source.replay_cells);
7127            report.source_forward_descendants = source.forward_descendants;
7128            report.source_evidence_limit_fallbacks = source.evidence_limit_fallbacks;
7129            report.source_evidence_peak_bytes = source.evidence_peak_bytes;
7130            for (reason, count) in source.fallback_reasons {
7131                let total = report.fallback_reasons.entry(reason).or_default();
7132                *total = total.saturating_add(count);
7133            }
7134        }
7135
7136        // A first load without graph admission plans in the builder, one
7137        // chunk at a time (a load error fails the load, so the builder's
7138        // partial application on a planning error is unobservable).
7139        if self.graph.first_load_assume_new()
7140            && !self.graph_admission_enabled()
7141            && !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
7142        {
7143            let mut builder =
7144                crate::engine::ingest_builder::BulkIngestBuilder::new(&mut self.graph);
7145            for batch in materialize_batches {
7146                if batch.is_empty() {
7147                    continue;
7148                }
7149                let sheet_id = builder
7150                    .add_sheet_checked(&batch.sheet_name)
7151                    .ok_or_else(|| {
7152                        ExcelError::new(ExcelErrorKind::Ref)
7153                            .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7154                    })?;
7155                builder.add_formula_refs(
7156                    sheet_id,
7157                    batch.formulas.into_iter().map(|record| {
7158                        (
7159                            record.row,
7160                            record.col,
7161                            crate::engine::graph::FormulaRef::of_ingested(
7162                                record.ast_id,
7163                                record.member_anchor,
7164                            ),
7165                        )
7166                    }),
7167                );
7168            }
7169            let summary = builder.finish_with_provider(&self.resolver)?;
7170            report.graph_formula_cells_materialized = summary.formulas as u64;
7171            report.graph_vertices_created = summary.vertices as u64;
7172            report.graph_edges_created = summary.edges as u64;
7173        } else if !materialize_batches.iter().all(FormulaIngestBatch::is_empty) {
7174            let mut prepared_by_sheet: BTreeMap<String, Vec<_>> = BTreeMap::new();
7175            for batch in materialize_batches {
7176                if batch.is_empty() {
7177                    continue;
7178                }
7179                let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
7180                    ExcelError::new(ExcelErrorKind::Ref)
7181                        .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7182                })?;
7183                let mut pipeline = self.ingest_pipeline();
7184                // Plan each record and keep only what the graph needs (the
7185                // pipeline's per-formula facts are dropped at once).
7186                let prepared = prepared_by_sheet.entry(batch.sheet_name).or_default();
7187                prepared.reserve(batch.formulas.len());
7188                for record in batch.formulas {
7189                    let placement = CellRef::new(
7190                        sheet_id,
7191                        Coord::from_excel(record.row, record.col, true, true),
7192                    );
7193                    let input = match record.member_anchor {
7194                        Some(anchor) => FormulaAstInput::Member {
7195                            template: record.ast_id,
7196                            anchor,
7197                        },
7198                        None => FormulaAstInput::RawArena(record.ast_id),
7199                    };
7200                    let formula = pipeline.ingest_formula(input, placement, None)?;
7201                    prepared.push((
7202                        record.row,
7203                        record.col,
7204                        crate::engine::graph::FormulaRef::of_ingested(
7205                            formula.ast_id,
7206                            formula.member_anchor,
7207                        ),
7208                        formula.dep_plan,
7209                    ));
7210                }
7211            }
7212            let admission_preflighted = self.graph_admission_enabled();
7213            if admission_preflighted {
7214                let mut preview = Vec::new();
7215                for (sheet_name, formulas) in &prepared_by_sheet {
7216                    let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(|| {
7217                        ExcelError::new(ExcelErrorKind::Ref)
7218                            .with_message(format!("unknown ingest sheet: {sheet_name}"))
7219                    })?;
7220                    preview.extend(
7221                        formulas
7222                            .iter()
7223                            .map(|(row, col, _, plan)| (sheet_id, *row, *col, plan.clone())),
7224                    );
7225                }
7226                let admission = self.graph.preview_formula_mutations(&preview)?;
7227                self.preflight_graph_admission(admission)?;
7228            }
7229
7230            let mut builder = self.begin_bulk_ingest();
7231            if admission_preflighted {
7232                builder.mark_admission_preflighted();
7233            }
7234            for (sheet_name, formulas) in prepared_by_sheet {
7235                if formulas.is_empty() {
7236                    continue;
7237                }
7238                let sheet_id = builder.add_sheet(&sheet_name);
7239                builder.add_formula_plans(sheet_id, formulas);
7240            }
7241            let summary = builder.finish()?;
7242            report.graph_formula_cells_materialized = summary.formulas as u64;
7243            report.graph_vertices_created = summary.vertices as u64;
7244            report.graph_edges_created = summary.edges as u64;
7245        }
7246
7247        crate::engine::trace::fz_event!(
7248            tracing::Level::INFO,
7249            "ingest",
7250            "ingest.summary",
7251            candidate_cells = report.shadow_candidate_cells,
7252            accepted_span_cells = report.shadow_accepted_span_cells,
7253            fallback_cells = report.shadow_fallback_cells,
7254            spans_created = report.shadow_spans_created,
7255            templates_interned = report.shadow_templates_interned,
7256            graph_vertices_created = report.graph_vertices_created,
7257            graph_edges_created = report.graph_edges_created,
7258            arena_nodes_delta = self
7259                .graph
7260                .data_store()
7261                .memory_usage()
7262                .total_ast_nodes
7263                .saturating_sub(arena_nodes_before)
7264        );
7265        if publish_report {
7266            self.record_formula_ingest_report(report.clone());
7267        }
7268        Ok(report)
7269    }
7270
7271    fn dedup_formula_parse_diagnostics_since(&mut self, start: usize) {
7272        let mut unique = Vec::new();
7273        for diagnostic in self.formula_parse_diagnostics.drain(start..) {
7274            let duplicate = unique.iter().any(|prior: &FormulaParseDiagnostic| {
7275                prior.sheet == diagnostic.sheet
7276                    && prior.row == diagnostic.row
7277                    && prior.col == diagnostic.col
7278                    && prior.formula == diagnostic.formula
7279                    && prior.policy == diagnostic.policy
7280            });
7281            if !duplicate {
7282                unique.push(diagnostic);
7283            }
7284        }
7285        self.formula_parse_diagnostics.extend(unique);
7286    }
7287
7288    pub fn handle_formula_parse_error(
7289        &mut self,
7290        sheet: &str,
7291        row: u32,
7292        col: u32,
7293        formula: &str,
7294        message: String,
7295    ) -> Result<Option<ASTNode>, ExcelError> {
7296        let policy = self.config.formula_parse_policy;
7297
7298        if policy == FormulaParsePolicy::Strict {
7299            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
7300            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
7301                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
7302            )));
7303        }
7304
7305        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
7306            sheet: sheet.to_string(),
7307            row,
7308            col,
7309            formula: formula.to_string(),
7310            message: message.clone(),
7311            policy,
7312        });
7313
7314        match policy {
7315            FormulaParsePolicy::Strict => unreachable!(),
7316            FormulaParsePolicy::KeepCachedValue => Ok(None),
7317            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
7318                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
7319                None,
7320            ))),
7321            FormulaParsePolicy::CoerceToError => {
7322                let err = ExcelError::new(ExcelErrorKind::Error)
7323                    .with_message(format!("Malformed formula: {message}"));
7324                Ok(Some(ASTNode::new(
7325                    ASTNodeType::Literal(LiteralValue::Error(err)),
7326                    None,
7327                )))
7328            }
7329        }
7330    }
7331
7332    #[cfg(test)]
7333    fn target_preparation_fault(
7334        &mut self,
7335        seam: crate::engine::target_preparation::TargetPreparationFault,
7336    ) -> Result<(), ExcelError> {
7337        if self.target_preparation_fault_for_test == Some(seam) {
7338            self.target_preparation_fault_for_test = None;
7339            Err(ExcelError::new(ExcelErrorKind::Value)
7340                .with_message(format!("injected target preparation fault: {seam:?}")))
7341        } else {
7342            Ok(())
7343        }
7344    }
7345
7346    fn preparation_stale(
7347        reason: formualizer_common::PreparationStaleReason,
7348        message: impl Into<String>,
7349    ) -> ExcelError {
7350        ExcelError::new(ExcelErrorKind::Value)
7351            .with_message(message)
7352            .with_extra(formualizer_common::ExcelErrorExtra::PreparationStale { reason })
7353    }
7354
7355    fn preparation_revision_stale_reason(
7356        assumptions: &crate::engine::PreparationRevision,
7357        current: &crate::engine::PreparationRevision,
7358        planning_requests: &BTreeSet<(String, String, usize)>,
7359        staged_leases_match: bool,
7360    ) -> Option<formualizer_common::PreparationStaleReason> {
7361        if assumptions.graph != current.graph {
7362            Some(formualizer_common::PreparationStaleReason::Graph)
7363        } else if assumptions.staged != current.staged || !staged_leases_match {
7364            Some(formualizer_common::PreparationStaleReason::Staged)
7365        } else if assumptions.symbols != current.symbols {
7366            Some(formualizer_common::PreparationStaleReason::Symbols)
7367        } else if assumptions.provider != current.provider {
7368            Some(formualizer_common::PreparationStaleReason::Provider)
7369        } else if assumptions.semantic != current.semantic
7370            && crate::function_registry::semantic_changes_affect_requests_since(
7371                assumptions.semantic,
7372                planning_requests.iter().cloned(),
7373            )
7374        {
7375            Some(formualizer_common::PreparationStaleReason::Semantic)
7376        } else {
7377            None
7378        }
7379    }
7380
7381    fn preparation_revisions(&self) -> crate::engine::PreparationRevision {
7382        crate::engine::PreparationRevision {
7383            graph: self.graph.topology_revision(),
7384            authority: 0,
7385            authority_indexes: 0,
7386            authority_indexed_plane: 0,
7387            staged: self.staged_formula_index.revision(),
7388            symbols: self.graph.symbol_revision(),
7389            semantic: crate::function_registry::semantic_epoch(),
7390            provider: self.resolver.planning_semantic_revision(),
7391        }
7392    }
7393
7394    fn planning_revision_snapshot(&self) -> PlanningRevisionSnapshot {
7395        let registry_guard = crate::function_registry::semantic_epoch_read_guard();
7396        let provider = self.resolver.planning_semantic_revision();
7397        let semantic = registry_guard.epoch();
7398        PlanningRevisionSnapshot {
7399            engine_topology_epoch: self.topology_epoch,
7400            graph_topology_revision: self.graph.topology_revision(),
7401            staged: self.staged_formula_index.revision(),
7402            symbols: self.graph.symbol_revision(),
7403            semantic,
7404            provider,
7405            deterministic_mode: self.config.deterministic_mode.clone(),
7406            budgets: self.evaluation_resource_budgets.clone(),
7407        }
7408    }
7409
7410    fn recalc_plan_key(&self) -> RecalcPlanKey {
7411        RecalcPlanKey {
7412            engine_token: Arc::clone(&self.recalc_plan_token),
7413            revisions: self.planning_revision_snapshot(),
7414        }
7415    }
7416
7417    fn plan_stale(reason: formualizer_common::PlanStaleReason) -> ExcelError {
7418        ExcelError::new(ExcelErrorKind::Value)
7419            .with_message(format!("recalculation plan is stale: {}", reason.as_str()))
7420            .with_extra(formualizer_common::ExcelErrorExtra::PlanStale { reason })
7421    }
7422
7423    fn validate_recalc_plan_key(&self, key: &RecalcPlanKey) -> Result<(), ExcelError> {
7424        use formualizer_common::PlanStaleReason;
7425
7426        if !Arc::ptr_eq(&key.engine_token, &self.recalc_plan_token) {
7427            return Err(Self::plan_stale(PlanStaleReason::Engine));
7428        }
7429
7430        let current = self.planning_revision_snapshot();
7431        let expected = &key.revisions;
7432        let stale = if expected.provider != current.provider {
7433            Some(PlanStaleReason::Provider)
7434        } else if expected.semantic != current.semantic {
7435            Some(PlanStaleReason::Semantic)
7436        } else if expected.budgets != current.budgets
7437            || expected.deterministic_mode != current.deterministic_mode
7438        {
7439            Some(PlanStaleReason::Budget)
7440        } else if expected.staged != current.staged {
7441            Some(PlanStaleReason::Staged)
7442        } else if expected.symbols != current.symbols {
7443            Some(PlanStaleReason::Symbols)
7444        } else if expected.graph_topology_revision != current.graph_topology_revision
7445            || expected.engine_topology_epoch != current.engine_topology_epoch
7446        {
7447            Some(PlanStaleReason::Graph)
7448        } else {
7449            None
7450        };
7451        stale.map_or(Ok(()), |reason| Err(Self::plan_stale(reason)))
7452    }
7453
7454    fn target_preparation_checkpoint(
7455        &mut self,
7456        deadline: Option<std::time::Instant>,
7457        work: u64,
7458    ) -> Result<(), ExcelError> {
7459        if self
7460            .active_cancel_flag
7461            .as_ref()
7462            .is_some_and(|cancel| cancel.is_cancelled())
7463        {
7464            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
7465                .with_message("target graph preparation cancelled"));
7466        }
7467        if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
7468            return Err(crate::engine::ResourceLedgerError::Exhausted(
7469                formualizer_common::ResourceExhaustionDetail {
7470                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
7471                    limit: 0,
7472                    observed: 1,
7473                    request_id: self
7474                        .active_evaluation_resource_request
7475                        .as_ref()
7476                        .map(|stats| stats.request_id),
7477                },
7478            )
7479            .into_excel_error());
7480        }
7481        self.charge_bounded_work(work)
7482    }
7483
7484    fn opaque_reason_in_ast(
7485        &self,
7486        ast: &ASTNode,
7487        provider: &dyn crate::traits::FunctionProvider,
7488    ) -> Option<crate::engine::OpaqueReason> {
7489        match &ast.node_type {
7490            ASTNodeType::Function { name, args } => {
7491                let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
7492                if canonical == "INDIRECT" {
7493                    return Some(crate::engine::OpaqueReason::RuntimeTextReference);
7494                }
7495                let Some(function) = provider.get_function_for_planning("", name) else {
7496                    return Some(crate::engine::OpaqueReason::UnknownFunction);
7497                };
7498                let caps = function.caps();
7499                if caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7500                    || caps.contains(FnCaps::RETURNS_REFERENCE)
7501                {
7502                    return Some(crate::engine::OpaqueReason::DynamicReference);
7503                }
7504                args.iter()
7505                    .find_map(|arg| self.opaque_reason_in_ast(arg, provider))
7506            }
7507            ASTNodeType::Call { .. } => Some(crate::engine::OpaqueReason::UnknownCustomFunction),
7508            ASTNodeType::UnaryOp { expr, .. } => self.opaque_reason_in_ast(expr, provider),
7509            ASTNodeType::BinaryOp { left, right, .. } => self
7510                .opaque_reason_in_ast(left, provider)
7511                .or_else(|| self.opaque_reason_in_ast(right, provider)),
7512            ASTNodeType::Array(rows) => rows
7513                .iter()
7514                .flat_map(|row| row.iter())
7515                .find_map(|item| self.opaque_reason_in_ast(item, provider)),
7516            ASTNodeType::Reference {
7517                reference:
7518                    ReferenceType::Cell {
7519                        sheet: Some(sheet), ..
7520                    }
7521                    | ReferenceType::Range {
7522                        sheet: Some(sheet), ..
7523                    },
7524                ..
7525            } if self.graph.sheet_id(sheet).is_none() => {
7526                Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding)
7527            }
7528            ASTNodeType::Reference {
7529                reference:
7530                    ReferenceType::External(_)
7531                    | ReferenceType::Cell3D { .. }
7532                    | ReferenceType::Range3D { .. },
7533                ..
7534            } => Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
7535            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => None,
7536        }
7537    }
7538
7539    fn target_planning_snapshot(
7540        &mut self,
7541        ast: &ASTNode,
7542        planning_requests: &mut BTreeSet<(String, String, usize)>,
7543    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
7544        #[cfg(test)]
7545        if std::mem::take(&mut self.inject_target_semantic_stale_once_for_test) {
7546            return Err(Self::preparation_stale(
7547                formualizer_common::PreparationStaleReason::Semantic,
7548                "injected target semantic preparation movement",
7549            ));
7550        }
7551        #[cfg(test)]
7552        if let Some(hook) = self.before_target_planning_snapshot_hook.take() {
7553            hook();
7554        }
7555        let mut requests = Vec::new();
7556        Self::collect_planning_function_requests(ast, &mut requests);
7557        requests.sort();
7558        requests.dedup();
7559        planning_requests.extend(requests.iter().cloned());
7560        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
7561            &self.resolver,
7562            requests,
7563        )
7564        .map_err(|error| {
7565            ExcelError::new(ExcelErrorKind::Value)
7566                .with_message(format!("target planning snapshot unavailable: {error:?}"))
7567        })
7568    }
7569
7570    fn target_planning_snapshot_stale_reason(
7571        snapshot: &crate::function_registry::RegistryPlanningSnapshot,
7572        assumptions: &crate::engine::PreparationRevision,
7573    ) -> Option<formualizer_common::PreparationStaleReason> {
7574        if snapshot
7575            .provider_revision()
7576            .is_some_and(|revision| Some(revision) != assumptions.provider)
7577        {
7578            Some(formualizer_common::PreparationStaleReason::Provider)
7579        } else if snapshot.epoch() != assumptions.semantic
7580            && snapshot.semantic_changes_affect_requests_since(assumptions.semantic)
7581        {
7582            Some(formualizer_common::PreparationStaleReason::Semantic)
7583        } else {
7584            None
7585        }
7586    }
7587
7588    fn widen_target_preparation(
7589        policy: crate::engine::OpaquePreparePolicy,
7590        scope: &mut crate::engine::PrepareScope,
7591        reasons: &mut Vec<crate::engine::OpaqueReason>,
7592        reason: crate::engine::OpaqueReason,
7593    ) -> Result<bool, ExcelError> {
7594        if policy == crate::engine::OpaquePreparePolicy::Error {
7595            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7596                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7597        }
7598        if !reasons.contains(&reason) {
7599            reasons.push(reason);
7600        }
7601        if !matches!(scope, crate::engine::PrepareScope::Workbook) {
7602            *scope = crate::engine::PrepareScope::Workbook;
7603            Ok(true)
7604        } else {
7605            Ok(false)
7606        }
7607    }
7608
7609    fn widen_target_preparation_to_sheet(
7610        policy: crate::engine::OpaquePreparePolicy,
7611        scope: &mut crate::engine::PrepareScope,
7612        reasons: &mut Vec<crate::engine::OpaqueReason>,
7613        reason: crate::engine::OpaqueReason,
7614        sheet: &str,
7615    ) -> Result<bool, ExcelError> {
7616        if policy == crate::engine::OpaquePreparePolicy::Error {
7617            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7618                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7619        }
7620        if !reasons.contains(&reason) {
7621            reasons.push(reason);
7622        }
7623        match scope {
7624            crate::engine::PrepareScope::Exact => {
7625                *scope = crate::engine::PrepareScope::Sheets(vec![sheet.to_string()]);
7626                Ok(true)
7627            }
7628            crate::engine::PrepareScope::Sheets(sheets) => {
7629                if sheets.iter().any(|candidate| candidate == sheet) {
7630                    Ok(false)
7631                } else {
7632                    sheets.push(sheet.to_string());
7633                    sheets.sort();
7634                    Ok(true)
7635                }
7636            }
7637            crate::engine::PrepareScope::Workbook => Ok(false),
7638        }
7639    }
7640
7641    fn ast_has_proven_sheet_local_dynamic(
7642        ast: &ASTNode,
7643        provider: &dyn crate::traits::FunctionProvider,
7644    ) -> bool {
7645        fn classify(ast: &ASTNode, provider: &dyn crate::traits::FunctionProvider) -> (bool, bool) {
7646            match &ast.node_type {
7647                ASTNodeType::Function { name, args } => {
7648                    let Some(function) = provider.get_function_for_planning("", name) else {
7649                        return (false, false);
7650                    };
7651                    let caps = function.caps();
7652                    let dynamic = caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7653                        || caps.contains(FnCaps::RETURNS_REFERENCE);
7654                    let canonical = name.rsplit('.').next().unwrap_or(name);
7655                    if dynamic
7656                        && !canonical.eq_ignore_ascii_case("OFFSET")
7657                        && !canonical.eq_ignore_ascii_case("INDEX")
7658                    {
7659                        return (false, true);
7660                    }
7661                    let mut has_dynamic = dynamic;
7662                    for arg in args {
7663                        let (safe, child_dynamic) = classify(arg, provider);
7664                        if !safe {
7665                            return (false, has_dynamic || child_dynamic);
7666                        }
7667                        has_dynamic |= child_dynamic;
7668                    }
7669                    (true, has_dynamic)
7670                }
7671                ASTNodeType::UnaryOp { expr, .. } => classify(expr, provider),
7672                ASTNodeType::BinaryOp { left, right, .. } => {
7673                    let (left_safe, left_dynamic) = classify(left, provider);
7674                    let (right_safe, right_dynamic) = classify(right, provider);
7675                    (left_safe && right_safe, left_dynamic || right_dynamic)
7676                }
7677                ASTNodeType::Array(rows) => {
7678                    let mut has_dynamic = false;
7679                    for item in rows.iter().flatten() {
7680                        let (safe, child_dynamic) = classify(item, provider);
7681                        if !safe {
7682                            return (false, has_dynamic || child_dynamic);
7683                        }
7684                        has_dynamic |= child_dynamic;
7685                    }
7686                    (true, has_dynamic)
7687                }
7688                ASTNodeType::Reference {
7689                    reference:
7690                        ReferenceType::Cell { sheet: None, .. }
7691                        | ReferenceType::Range { sheet: None, .. },
7692                    ..
7693                }
7694                | ASTNodeType::Literal(_)
7695                | ASTNodeType::Omitted => (true, false),
7696                ASTNodeType::Call { .. } | ASTNodeType::Reference { .. } => (false, false),
7697            }
7698        }
7699
7700        let (safe, dynamic) = classify(ast, provider);
7701        safe && dynamic
7702    }
7703
7704    fn table_selection_region(
7705        &self,
7706        entry: &crate::engine::graph::TableEntry,
7707        selection: &crate::engine::TableSelection,
7708    ) -> Result<PreparationRegion, ExcelError> {
7709        let mut start_row = entry.range.start.coord.row() + 1;
7710        let mut end_row = entry.range.end.coord.row() + 1;
7711        let mut start_col = entry.range.start.coord.col() + 1;
7712        let mut end_col = entry.range.end.coord.col() + 1;
7713        match selection {
7714            crate::engine::TableSelection::Whole => {}
7715            crate::engine::TableSelection::Headers => {
7716                if !entry.header_row {
7717                    return Err(ExcelError::new(ExcelErrorKind::Value)
7718                        .with_message(format!("table {} has no header row", entry.name)));
7719                }
7720                end_row = start_row;
7721            }
7722            crate::engine::TableSelection::Data => {
7723                if entry.header_row {
7724                    start_row = start_row.saturating_add(1);
7725                }
7726                if entry.totals_row {
7727                    end_row = end_row.saturating_sub(1);
7728                }
7729            }
7730            crate::engine::TableSelection::Totals => {
7731                if !entry.totals_row {
7732                    return Err(ExcelError::new(ExcelErrorKind::Value)
7733                        .with_message(format!("table {} has no totals row", entry.name)));
7734                }
7735                start_row = end_row;
7736            }
7737            crate::engine::TableSelection::Column(column) => {
7738                let index = entry.col_index(column).ok_or_else(|| {
7739                    ExcelError::new(ExcelErrorKind::Name)
7740                        .with_message(format!("unknown table column: {column}"))
7741                })?;
7742                start_col = start_col.saturating_add(index as u32);
7743                end_col = start_col;
7744            }
7745            crate::engine::TableSelection::Columns { start, end } => {
7746                let first = entry.col_index(start).ok_or_else(|| {
7747                    ExcelError::new(ExcelErrorKind::Name)
7748                        .with_message(format!("unknown table column: {start}"))
7749                })?;
7750                let last = entry.col_index(end).ok_or_else(|| {
7751                    ExcelError::new(ExcelErrorKind::Name)
7752                        .with_message(format!("unknown table column: {end}"))
7753                })?;
7754                if first > last {
7755                    return Err(ExcelError::new(ExcelErrorKind::Value)
7756                        .with_message("table column selection is reversed"));
7757                }
7758                start_col = start_col.saturating_add(first as u32);
7759                end_col = entry
7760                    .range
7761                    .start
7762                    .coord
7763                    .col()
7764                    .saturating_add(last as u32)
7765                    .saturating_add(1);
7766            }
7767        }
7768        if start_row > end_row && matches!(selection, crate::engine::TableSelection::Data) {
7769            start_row = entry.range.start.coord.row() + 1;
7770            end_row = start_row;
7771        }
7772        if start_row > end_row || start_col > end_col {
7773            return Err(
7774                ExcelError::new(ExcelErrorKind::Value).with_message("table selection is empty")
7775            );
7776        }
7777        Ok(PreparationRegion {
7778            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
7779            sheet_id: entry.sheet_id(),
7780            start_row,
7781            start_col,
7782            end_row,
7783            end_col,
7784        })
7785    }
7786
7787    #[cfg(test)]
7788    pub(crate) fn reset_target_root_dedup_probes_for_test() {
7789        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(0));
7790    }
7791
7792    #[cfg(test)]
7793    pub(crate) fn target_root_dedup_probes_for_test() -> usize {
7794        TARGET_ROOT_DEDUP_PROBES.with(std::cell::Cell::get)
7795    }
7796
7797    pub(crate) fn resolve_target_producers(
7798        &mut self,
7799        targets: &[crate::engine::EvaluationTarget],
7800    ) -> Result<Vec<crate::engine::target_preparation::TargetProducer>, ExcelError> {
7801        use crate::engine::target_preparation::TargetProducer;
7802
7803        let request_id = self
7804            .active_evaluation_resource_request
7805            .as_ref()
7806            .map(|request| request.request_id);
7807        let mut roots = OrderedTargetProducers::with_capacity(targets.len())
7808            .map_err(|_| target_root_allocation_error(targets.len(), request_id))?;
7809        let resolve_region = |engine: &mut Self,
7810                              region: Region,
7811                              value_only: Option<CellRef>,
7812                              roots: &mut OrderedTargetProducers|
7813         -> Result<(), ExcelError> {
7814            let before = roots.len();
7815            for anchor in engine.graph.spill_anchors_in_region(
7816                region.sheet_id(),
7817                region.axis_ranges().0.query_bounds().0,
7818                region.axis_ranges().1.query_bounds().0,
7819                region.axis_ranges().0.query_bounds().1,
7820                region.axis_ranges().1.query_bounds().1,
7821            ) {
7822                roots
7823                    .push(TargetProducer::Legacy(anchor))
7824                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7825            }
7826            for vertex in engine.graph.vertices_in_region(
7827                region.sheet_id(),
7828                region.axis_ranges().0.query_bounds().0,
7829                region.axis_ranges().0.query_bounds().1,
7830                region.axis_ranges().1.query_bounds().0,
7831                region.axis_ranges().1.query_bounds().1,
7832            ) {
7833                let vertex = engine
7834                    .graph
7835                    .get_cell_ref(vertex)
7836                    .and_then(|cell| engine.graph.spill_registry_anchor_for_cell(cell))
7837                    .unwrap_or(vertex);
7838                // `vertices_in_region` is a sheet-index query and a sheet index holds only
7839                // grid-addressed vertices, so a region can never yield a symbol: names,
7840                // tables and external sources have no position for a region to cover.
7841                // Symbol roots come from the by-name lookups below instead.
7842                if matches!(
7843                    engine.graph.get_vertex_kind(vertex),
7844                    VertexKind::FormulaScalar | VertexKind::FormulaArray
7845                ) {
7846                    roots
7847                        .push(TargetProducer::Legacy(vertex))
7848                        .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7849                }
7850            }
7851            if roots.len() == before
7852                && let Some(cell) = value_only
7853            {
7854                roots
7855                    .push(TargetProducer::ValueOnly(cell))
7856                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
7857            }
7858            Ok(())
7859        };
7860
7861        for target in targets {
7862            match target {
7863                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
7864                    if *row == 0 || *col == 0 {
7865                        return Err(ExcelError::new(ExcelErrorKind::Ref)
7866                            .with_message("target cell coordinates are one-based"));
7867                    }
7868                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
7869                        ExcelError::new(ExcelErrorKind::Ref)
7870                            .with_message(format!("target sheet not found: {sheet}"))
7871                    })?;
7872                    let cell = CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true));
7873                    resolve_region(
7874                        self,
7875                        Region::point(sheet_id, *row - 1, *col - 1),
7876                        Some(cell),
7877                        &mut roots,
7878                    )?;
7879                }
7880                crate::engine::EvaluationTarget::Range(range) => {
7881                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
7882                        ExcelError::new(ExcelErrorKind::Ref)
7883                            .with_message(format!("target sheet not found: {}", range.sheet))
7884                    })?;
7885                    resolve_region(
7886                        self,
7887                        Region::rect(
7888                            sheet_id,
7889                            range.start_row - 1,
7890                            range.end_row - 1,
7891                            range.start_col - 1,
7892                            range.end_col - 1,
7893                        ),
7894                        None,
7895                        &mut roots,
7896                    )?;
7897                }
7898                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
7899                    let scope = self.name_query_scope(scope_sheet.as_deref())?;
7900                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, scope) {
7901                        roots
7902                            .push(TargetProducer::Symbol(entry.vertex))
7903                            .map_err(|_| {
7904                                target_root_allocation_error(roots.len() + 1, request_id)
7905                            })?;
7906                    }
7907                }
7908                crate::engine::EvaluationTarget::Table { name, .. } => {
7909                    if let Some(entry) = self.graph.resolve_table_entry(name) {
7910                        roots
7911                            .push(TargetProducer::Symbol(entry.vertex))
7912                            .map_err(|_| {
7913                                target_root_allocation_error(roots.len() + 1, request_id)
7914                            })?;
7915                    }
7916                }
7917            }
7918        }
7919        Ok(roots.into_vec())
7920    }
7921
7922    /// Transactionally prepare the complete ordinary staged demand closure for typed targets.
7923    ///
7924    /// This method prepares graph topology only. It does not evaluate target values.
7925    pub fn prepare_graph_for_targets(
7926        &mut self,
7927        targets: &[crate::engine::EvaluationTarget],
7928        options: crate::engine::TargetEvalOptions<'_>,
7929    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7930        let previous_budgets = self.evaluation_resource_budgets.clone();
7931        let diagnostics_len = self.formula_parse_diagnostics.len();
7932        let previous_report = self.last_formula_ingest_report.clone();
7933        if let Some(budgets) = options.budgets {
7934            self.evaluation_resource_budgets = budgets.clone();
7935        }
7936        let previous_graph_budget_override = self
7937            .graph
7938            .set_admission_budget_override(Some(self.evaluation_resource_budgets.clone()));
7939        // Hoist the call's cancellation onto the engine for its duration, so the
7940        // preparation checkpoints observe it. This is a standalone entry point, so
7941        // the previous value is restored rather than cleared.
7942        let previous_cancel = self.active_cancel_flag.take();
7943        self.active_cancel_flag = options.cancel.clone();
7944        let result = self.observe_evaluation_resource_request(
7945            EvaluationRequestKind::TargetPreparation,
7946            |engine| engine.prepare_graph_for_targets_unobserved(targets, &options),
7947        );
7948        self.active_cancel_flag = previous_cancel;
7949        self.graph
7950            .set_admission_budget_override(previous_graph_budget_override);
7951        self.evaluation_resource_budgets = previous_budgets;
7952        if result.is_err() {
7953            self.formula_parse_diagnostics.truncate(diagnostics_len);
7954            self.last_formula_ingest_report = previous_report;
7955        }
7956        result
7957    }
7958
7959    fn prepare_graph_for_targets_unobserved(
7960        &mut self,
7961        targets: &[crate::engine::EvaluationTarget],
7962        options: &crate::engine::TargetEvalOptions<'_>,
7963    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7964        let scratch_checkpoint = self
7965            .active_resource_ledger
7966            .as_ref()
7967            .map_or(0, crate::engine::ResourceLedger::scratch_checkpoint);
7968        let result = self.prepare_graph_for_targets_transaction(targets, options);
7969        let release = self
7970            .active_resource_ledger
7971            .as_mut()
7972            .map_or(Ok(()), |ledger| {
7973                ledger.release_scratch_to(scratch_checkpoint)
7974            });
7975        match (result, release) {
7976            (result, Ok(())) => result,
7977            (Ok(_), Err(error)) | (Err(_), Err(error)) => Err(error.into_excel_error()),
7978        }
7979    }
7980
7981    fn prepare_graph_for_targets_transaction(
7982        &mut self,
7983        targets: &[crate::engine::EvaluationTarget],
7984        options: &crate::engine::TargetEvalOptions<'_>,
7985    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
7986        use crate::engine::{
7987            OpaqueReason, PreparationOutcome, PrepareScope, PreparedTargetGraphReport,
7988            TableSelection,
7989        };
7990
7991        self.target_preparation_checkpoint(options.deadline, 0)?;
7992        self.observe_function_semantic_epoch()?;
7993        let assumptions = self.preparation_revisions();
7994        let ledger_at_start = self
7995            .active_resource_ledger
7996            .as_ref()
7997            .map(|ledger| ledger.snapshot());
7998        let diagnostics_len = self.formula_parse_diagnostics.len();
7999        let report_len = self.last_formula_ingest_report.clone();
8000        let request_id = options.request_id.or_else(|| {
8001            self.active_evaluation_resource_request
8002                .as_ref()
8003                .map(|stats| stats.request_id)
8004        });
8005
8006        let mut scope = PrepareScope::Exact;
8007        let mut reasons = Vec::new();
8008        let mut regions = VecDeque::new();
8009        let mut deferred_shared_regions = VecDeque::new();
8010        let mut normalized = Vec::with_capacity(targets.len());
8011        let mut symbol_vertices = VecDeque::new();
8012        for target in targets {
8013            self.target_preparation_checkpoint(options.deadline, 1)?;
8014            match target {
8015                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
8016                    if *row == 0 || *col == 0 {
8017                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8018                            .with_message("target cell coordinates are one-based"));
8019                    }
8020                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
8021                        ExcelError::new(ExcelErrorKind::Ref)
8022                            .with_message(format!("target sheet not found: {sheet}"))
8023                    })?;
8024                    regions.push_back(PreparationRegion {
8025                        sheet: sheet.clone(),
8026                        sheet_id,
8027                        start_row: *row,
8028                        start_col: *col,
8029                        end_row: *row,
8030                        end_col: *col,
8031                    });
8032                    normalized.push(target.clone());
8033                }
8034                crate::engine::EvaluationTarget::Range(range) => {
8035                    if range.start_row == 0
8036                        || range.start_col == 0
8037                        || range.end_row < range.start_row
8038                        || range.end_col < range.start_col
8039                    {
8040                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8041                            .with_message("invalid target range"));
8042                    }
8043                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8044                        ExcelError::new(ExcelErrorKind::Ref)
8045                            .with_message(format!("target sheet not found: {}", range.sheet))
8046                    })?;
8047                    regions.push_back(PreparationRegion {
8048                        sheet: range.sheet.clone(),
8049                        sheet_id,
8050                        start_row: range.start_row,
8051                        start_col: range.start_col,
8052                        end_row: range.end_row,
8053                        end_col: range.end_col,
8054                    });
8055                    normalized.push(target.clone());
8056                }
8057                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
8058                    let name_scope = self.name_query_scope(scope_sheet.as_deref())?;
8059                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, name_scope) {
8060                        symbol_vertices.push_back(entry.vertex);
8061                    } else {
8062                        Self::widen_target_preparation(
8063                            options.opaque_policy,
8064                            &mut scope,
8065                            &mut reasons,
8066                            OpaqueReason::UnresolvedName,
8067                        )?;
8068                    }
8069                    normalized.push(target.clone());
8070                }
8071                crate::engine::EvaluationTarget::Table { name, selection } => {
8072                    if let Some(entry) = self.graph.resolve_table_entry(name) {
8073                        let region = self.table_selection_region(entry, selection)?;
8074                        symbol_vertices.push_back(entry.vertex);
8075                        regions.push_back(region);
8076                    } else {
8077                        Self::widen_target_preparation(
8078                            options.opaque_policy,
8079                            &mut scope,
8080                            &mut reasons,
8081                            OpaqueReason::UnresolvedTable,
8082                        )?;
8083                    }
8084                    normalized.push(target.clone());
8085                }
8086            }
8087        }
8088
8089        let mut visited_regions = FxHashSet::default();
8090        let mut visited_vertices = FxHashSet::default();
8091        let mut selected = FxHashSet::default();
8092        let mut prepared = Vec::new();
8093        let mut pending_diagnostics = Vec::new();
8094        let mut planning_requests = BTreeSet::new();
8095        let mut selected_cells = Vec::new();
8096        let mut workbook_seeded = false;
8097        let mut sheet_scope_seeded = BTreeSet::new();
8098        let mut indexed_query_sheets = FxHashSet::default();
8099        let mut discovery_scratch_reserved = 0u64;
8100        let mut package_encountered = false;
8101        let mut selected_package_sheets = FxHashSet::default();
8102        let mut selected_package_points: BTreeMap<String, BTreeSet<(u32, u32)>> = BTreeMap::new();
8103        let mut prepared_packages: Vec<PreparedTargetSourcePackage> = Vec::new();
8104        let authoritative_with_ordinary = false;
8105        let has_unknown_package_sheet = self
8106            .staged_formula_index
8107            .package_sheets()
8108            .any(|sheet| self.graph.sheet_id(sheet).is_none());
8109
8110        loop {
8111            if let PrepareScope::Sheets(sheets) = &scope {
8112                for sheet in sheets.clone() {
8113                    if !sheet_scope_seeded.insert(sheet.clone()) {
8114                        continue;
8115                    }
8116                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8117                        package_encountered = true;
8118                        Self::widen_target_preparation(
8119                            options.opaque_policy,
8120                            &mut scope,
8121                            &mut reasons,
8122                            OpaqueReason::UnsupportedSourceSemantics,
8123                        )?;
8124                        continue;
8125                    };
8126                    for lease in self.staged_formula_index.leases_for_sheet(&sheet) {
8127                        self.target_preparation_checkpoint(options.deadline, 1)?;
8128                        regions.push_back(PreparationRegion {
8129                            sheet: sheet.clone(),
8130                            sheet_id,
8131                            start_row: lease.row,
8132                            start_col: lease.col,
8133                            end_row: lease.row,
8134                            end_col: lease.col,
8135                        });
8136                    }
8137                    if self
8138                        .staged_formula_index
8139                        .package_lease_for_sheet(&sheet)
8140                        .is_some()
8141                    {
8142                        regions.push_back(PreparationRegion {
8143                            sheet: sheet.clone(),
8144                            sheet_id,
8145                            start_row: 1,
8146                            start_col: 1,
8147                            end_row: self.workbook_load_limits.max_sheet_rows,
8148                            end_col: self.workbook_load_limits.max_sheet_cols,
8149                        });
8150                    }
8151                }
8152            }
8153            if matches!(scope, PrepareScope::Workbook) && !workbook_seeded {
8154                workbook_seeded = true;
8155                for (sheet, lease) in self.staged_formula_index.all_leases() {
8156                    self.target_preparation_checkpoint(options.deadline, 1)?;
8157                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8158                        package_encountered = true;
8159                        Self::widen_target_preparation(
8160                            options.opaque_policy,
8161                            &mut scope,
8162                            &mut reasons,
8163                            OpaqueReason::UnsupportedSourceSemantics,
8164                        )?;
8165                        continue;
8166                    };
8167                    regions.push_back(PreparationRegion {
8168                        sheet,
8169                        sheet_id,
8170                        start_row: lease.row,
8171                        start_col: lease.col,
8172                        end_row: lease.row,
8173                        end_col: lease.col,
8174                    });
8175                }
8176                let package_sheets = self
8177                    .staged_formula_index
8178                    .package_sheets()
8179                    .map(str::to_string)
8180                    .collect::<Vec<_>>();
8181                for sheet in package_sheets {
8182                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8183                        package_encountered = true;
8184                        Self::widen_target_preparation(
8185                            options.opaque_policy,
8186                            &mut scope,
8187                            &mut reasons,
8188                            OpaqueReason::UnsupportedSourceSemantics,
8189                        )?;
8190                        continue;
8191                    };
8192                    regions.push_back(PreparationRegion {
8193                        sheet,
8194                        sheet_id,
8195                        start_row: 1,
8196                        start_col: 1,
8197                        end_row: self.workbook_load_limits.max_sheet_rows,
8198                        end_col: self.workbook_load_limits.max_sheet_cols,
8199                    });
8200                }
8201            }
8202
8203            // Finish known ordinary/name dependency discovery before expanding
8204            // partial shared demands. A queued SUM may complete those families.
8205            let allow_partial_shared = regions.is_empty() && symbol_vertices.is_empty();
8206            let next_region = if allow_partial_shared {
8207                deferred_shared_regions.pop_front()
8208            } else {
8209                regions.pop_front()
8210            };
8211            let Some(region) = next_region else {
8212                if let Some(vertex) = symbol_vertices.pop_front() {
8213                    self.target_preparation_checkpoint(options.deadline, 1)?;
8214                    if !visited_vertices.insert(vertex) || !self.graph.vertex_exists(vertex) {
8215                        continue;
8216                    }
8217                    let vertex_is_dynamic = self.graph.is_dynamic(vertex);
8218                    if let Some(ast) = self.graph.get_formula(vertex) {
8219                        let snapshot =
8220                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8221                        if let Some(reason) =
8222                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8223                        {
8224                            return Err(Self::preparation_stale(
8225                                reason,
8226                                "target planning snapshot became stale during discovery",
8227                            ));
8228                        }
8229                        let opaque = self.opaque_reason_in_ast(&ast, &snapshot);
8230                        if let Some(reason) =
8231                            opaque.or(vertex_is_dynamic.then_some(OpaqueReason::DynamicReference))
8232                        {
8233                            if reason == OpaqueReason::DynamicReference
8234                                && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
8235                            {
8236                                let sheet = self.graph.get_vertex_sheet_id(vertex);
8237                                let sheet = self.graph.sheet_name(sheet).to_string();
8238                                Self::widen_target_preparation_to_sheet(
8239                                    options.opaque_policy,
8240                                    &mut scope,
8241                                    &mut reasons,
8242                                    reason,
8243                                    &sheet,
8244                                )?;
8245                            } else {
8246                                Self::widen_target_preparation(
8247                                    options.opaque_policy,
8248                                    &mut scope,
8249                                    &mut reasons,
8250                                    reason,
8251                                )?;
8252                            }
8253                        }
8254                    } else if vertex_is_dynamic {
8255                        Self::widen_target_preparation(
8256                            options.opaque_policy,
8257                            &mut scope,
8258                            &mut reasons,
8259                            OpaqueReason::DynamicReference,
8260                        )?;
8261                    }
8262                    if let Some(anchor) = self
8263                        .graph
8264                        .get_cell_ref(vertex)
8265                        .and_then(|cell| self.graph.spill_registry_anchor_for_cell(cell))
8266                    {
8267                        symbol_vertices.push_back(anchor);
8268                    }
8269                    if let Some(cell) = self.graph.get_cell_ref(vertex) {
8270                        let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
8271                        regions.push_back(PreparationRegion {
8272                            sheet,
8273                            sheet_id: cell.sheet_id,
8274                            start_row: cell.coord.row() + 1,
8275                            start_col: cell.coord.col() + 1,
8276                            end_row: cell.coord.row() + 1,
8277                            end_col: cell.coord.col() + 1,
8278                        });
8279                    }
8280                    // The formula's direct precedents, from the authority:
8281                    // cells and ranges become regions, symbol rows (names,
8282                    // tables, sources) their vertices.
8283                    match self.graph.authority_vertex_precedents(vertex) {
8284                        Some(precedents) => {
8285                            for (sheet_id, rect) in precedents {
8286                                self.target_preparation_checkpoint(options.deadline, 1)?;
8287                                if sheet_id == crate::engine::authority::geom::SYMBOL_SHEET {
8288                                    for slot in rect.r0..=rect.r1 {
8289                                        if let Some(symbol) =
8290                                            self.graph.authority_host().symbols().vertex(slot)
8291                                        {
8292                                            symbol_vertices.push_back(symbol);
8293                                        }
8294                                    }
8295                                    continue;
8296                                }
8297                                let sheet = self.graph.sheet_name(sheet_id).to_string();
8298                                regions.push_back(PreparationRegion {
8299                                    sheet,
8300                                    sheet_id,
8301                                    start_row: rect.r0 + 1,
8302                                    start_col: rect.c0 + 1,
8303                                    end_row: (rect.r1 + 1)
8304                                        .min(self.workbook_load_limits.max_sheet_rows),
8305                                    end_col: (rect.c1 + 1)
8306                                        .min(self.workbook_load_limits.max_sheet_cols),
8307                                });
8308                            }
8309                        }
8310                        // The authority cannot answer (failed host): widen.
8311                        None => {
8312                            if Self::widen_target_preparation(
8313                                options.opaque_policy,
8314                                &mut scope,
8315                                &mut reasons,
8316                                OpaqueReason::UnresolvedCrossSheetBinding,
8317                            )? {
8318                                continue;
8319                            }
8320                        }
8321                    }
8322                    if let Some(name) = self.graph.named_range_by_vertex(vertex).cloned() {
8323                        match &name.definition {
8324                            NamedDefinition::Cell(cell) => regions.push_back(PreparationRegion {
8325                                sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
8326                                sheet_id: cell.sheet_id,
8327                                start_row: cell.coord.row() + 1,
8328                                start_col: cell.coord.col() + 1,
8329                                end_row: cell.coord.row() + 1,
8330                                end_col: cell.coord.col() + 1,
8331                            }),
8332                            NamedDefinition::Range(range) => regions.push_back(PreparationRegion {
8333                                sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
8334                                sheet_id: range.start.sheet_id,
8335                                start_row: range.start.coord.row() + 1,
8336                                start_col: range.start.coord.col() + 1,
8337                                end_row: range.end.coord.row() + 1,
8338                                end_col: range.end.coord.col() + 1,
8339                            }),
8340                            NamedDefinition::Formula {
8341                                ast,
8342                                dependencies,
8343                                range_deps,
8344                            } => {
8345                                let snapshot =
8346                                    self.target_planning_snapshot(ast, &mut planning_requests)?;
8347                                if let Some(reason) = Self::target_planning_snapshot_stale_reason(
8348                                    &snapshot,
8349                                    &assumptions,
8350                                ) {
8351                                    return Err(Self::preparation_stale(
8352                                        reason,
8353                                        "target planning snapshot became stale during discovery",
8354                                    ));
8355                                }
8356                                if let Some(reason) = self.opaque_reason_in_ast(ast, &snapshot) {
8357                                    Self::widen_target_preparation(
8358                                        options.opaque_policy,
8359                                        &mut scope,
8360                                        &mut reasons,
8361                                        reason,
8362                                    )?;
8363                                }
8364                                for dependency in dependencies {
8365                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8366                                    symbol_vertices.push_back(*dependency);
8367                                }
8368                                for range in range_deps {
8369                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8370                                    // `Current` is the sheet this name's formula
8371                                    // was interpreted on, which is the sheet its
8372                                    // vertex is placed on -- the same derivation
8373                                    // the formula-vertex arm above uses. It is
8374                                    // never the workbook default sheet, and an
8375                                    // unresolvable `Name` widens instead of
8376                                    // silently landing on some other sheet.
8377                                    let context_sheet = self.graph.get_vertex_sheet_id(vertex);
8378                                    let Ok(sheet_id) =
8379                                        self.resolve_sheet_locator(&range.sheet, context_sheet)
8380                                    else {
8381                                        if Self::widen_target_preparation(
8382                                            options.opaque_policy,
8383                                            &mut scope,
8384                                            &mut reasons,
8385                                            OpaqueReason::UnresolvedCrossSheetBinding,
8386                                        )? {
8387                                            break;
8388                                        }
8389                                        continue;
8390                                    };
8391                                    regions.push_back(PreparationRegion {
8392                                        sheet: self.graph.sheet_name(sheet_id).to_string(),
8393                                        sheet_id,
8394                                        start_row: range
8395                                            .start_row
8396                                            .map_or(1, |bound| bound.index + 1),
8397                                        start_col: range
8398                                            .start_col
8399                                            .map_or(1, |bound| bound.index + 1),
8400                                        end_row: range.end_row.map_or(
8401                                            self.workbook_load_limits.max_sheet_rows,
8402                                            |bound| bound.index + 1,
8403                                        ),
8404                                        end_col: range.end_col.map_or(
8405                                            self.workbook_load_limits.max_sheet_cols,
8406                                            |bound| bound.index + 1,
8407                                        ),
8408                                    });
8409                                }
8410                            }
8411                            NamedDefinition::Literal(_) => {}
8412                        }
8413                    }
8414                    if let Some(table) = self.graph.table_by_vertex(vertex) {
8415                        regions
8416                            .push_back(self.table_selection_region(table, &TableSelection::Whole)?);
8417                    }
8418                    continue;
8419                }
8420                break;
8421            };
8422
8423            self.target_preparation_checkpoint(options.deadline, 1)?;
8424            if !visited_regions.insert(region.clone()) && !allow_partial_shared {
8425                continue;
8426            }
8427            if let PrepareScope::Sheets(sheets) = &mut scope
8428                && !sheets.iter().any(|sheet| sheet == &region.sheet)
8429            {
8430                sheets.push(region.sheet.clone());
8431                sheets.sort();
8432            }
8433            let package_match = self.staged_formula_index.package_for_region(
8434                &region.sheet,
8435                region.start_row,
8436                region.start_col,
8437                region.end_row,
8438                region.end_col,
8439            );
8440            let package_lease = match package_match {
8441                Some(Ok(lease)) => Some(lease),
8442                Some(Err(()))
8443                    if region.start_row == 1
8444                        && region.start_col == 1
8445                        && region.end_row == self.workbook_load_limits.max_sheet_rows
8446                        && region.end_col == self.workbook_load_limits.max_sheet_cols =>
8447                {
8448                    self.staged_formula_index
8449                        .package_lease_for_sheet(&region.sheet)
8450                }
8451                Some(Err(())) => {
8452                    Self::widen_target_preparation(
8453                        options.opaque_policy,
8454                        &mut scope,
8455                        &mut reasons,
8456                        OpaqueReason::DeferredSourcePackage,
8457                    )?;
8458                    None
8459                }
8460                None => None,
8461            };
8462            let compatibility_before_package_replay = package_lease.is_some()
8463                && (authoritative_with_ordinary || has_unknown_package_sheet);
8464            let package_lease = if compatibility_before_package_replay {
8465                package_encountered = true;
8466                Self::widen_target_preparation(
8467                    options.opaque_policy,
8468                    &mut scope,
8469                    &mut reasons,
8470                    OpaqueReason::UnsupportedSourceSemantics,
8471                )?;
8472                None
8473            } else {
8474                package_lease
8475            };
8476            if let Some(package_lease) = package_lease
8477                && !selected_package_sheets.contains(&region.sheet)
8478            {
8479                self.target_preparation_checkpoint(options.deadline, 1)?;
8480                let mut points = self.staged_formula_index.package_points_in_region(
8481                    &region.sheet,
8482                    region.start_row,
8483                    region.start_col,
8484                    region.end_row,
8485                    region.end_col,
8486                );
8487                if let Some(selected) = selected_package_points.get(&region.sheet) {
8488                    points.retain(|point| !selected.contains(point));
8489                }
8490
8491                if let Some(selected) = selected_package_points.get(&region.sheet) {
8492                    points.retain(|point| !selected.contains(point));
8493                }
8494                let permit_partial = allow_partial_shared
8495                    || (points.len() == 1
8496                        && regions.is_empty()
8497                        && symbol_vertices.is_empty()
8498                        && deferred_shared_regions.is_empty());
8499                let partial = self.prepare_target_exact_source_selection(
8500                    &region.sheet,
8501                    package_lease,
8502                    points,
8503                    selected_package_points
8504                        .get(&region.sheet)
8505                        .unwrap_or(&BTreeSet::new()),
8506                    permit_partial,
8507                    options.deadline,
8508                    &mut discovery_scratch_reserved,
8509                )?;
8510                let mut package = if let Some(mut package) = partial {
8511                    if package.deferred_shared {
8512                        deferred_shared_regions.push_back(region.clone());
8513                    }
8514                    // A later range can complete a family touched earlier in this
8515                    // request. Retire only that family's earlier legacy proposals;
8516                    // nothing has been published and its dependencies remain demanded.
8517                    if !package.direct_domains.is_empty() {
8518                        for prior in prepared_packages
8519                            .iter_mut()
8520                            .filter(|prior| prior.sheet == region.sheet)
8521                        {
8522                            prior
8523                                .replay_records
8524                                .retain(|record| !package.direct_contains(record.row, record.col));
8525                            prior
8526                                .legacy
8527                                .retain(|(row, col, _, _)| !package.direct_contains(*row, *col));
8528                            if let Some(points) = prior.selected_points.as_mut() {
8529                                points.retain(|&(row, col)| !package.direct_contains(row, col));
8530                            }
8531                        }
8532                    }
8533                    if selected_package_points.contains_key(&region.sheet) {
8534                        package.source_report = Default::default();
8535                    }
8536                    let points = package.selected_points.as_ref().unwrap();
8537                    if !points.is_empty() {
8538                        selected_package_points
8539                            .entry(region.sheet.clone())
8540                            .or_default()
8541                            .extend(points.iter().copied());
8542                    }
8543                    package
8544                } else {
8545                    selected_package_sheets.insert(region.sheet.clone());
8546                    self.prepare_target_source_package(
8547                        &region.sheet,
8548                        package_lease,
8549                        options.deadline,
8550                    )?
8551                };
8552                if package
8553                    .selected_points
8554                    .as_ref()
8555                    .is_none_or(|points| !points.is_empty())
8556                {
8557                    let mut final_fallback = BTreeMap::new();
8558                    for record in package.fallback_records() {
8559                        final_fallback.insert((record.row, record.col), record.clone());
8560                    }
8561                    let batch = self.formula_batch_from_exact_replay(
8562                        &region.sheet,
8563                        final_fallback.into_values(),
8564                    )?;
8565                    for record in batch.formulas {
8566                        self.target_preparation_checkpoint(options.deadline, 1)?;
8567                        let ast = self
8568                            .graph
8569                            .data_store()
8570                            .retrieve_ast(record.ast_id, self.graph.sheet_reg())
8571                            .ok_or_else(|| {
8572                                ExcelError::new(ExcelErrorKind::Value)
8573                                    .with_message("target fallback AST is unavailable")
8574                            })?;
8575                        let snapshot =
8576                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8577                        if let Some(reason) =
8578                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8579                        {
8580                            return Err(Self::preparation_stale(
8581                                reason,
8582                                "target fallback planning snapshot became stale during discovery",
8583                            ));
8584                        }
8585                        let proven_sheet_local_dynamic =
8586                            Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
8587                        if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
8588                            if reason == OpaqueReason::DynamicReference
8589                                && proven_sheet_local_dynamic
8590                            {
8591                                Self::widen_target_preparation_to_sheet(
8592                                    options.opaque_policy,
8593                                    &mut scope,
8594                                    &mut reasons,
8595                                    reason,
8596                                    &region.sheet,
8597                                )?;
8598                            } else {
8599                                Self::widen_target_preparation(
8600                                    options.opaque_policy,
8601                                    &mut scope,
8602                                    &mut reasons,
8603                                    reason,
8604                                )?;
8605                            }
8606                        }
8607                        let placement = CellRef::new(
8608                            package.sheet_id,
8609                            Coord::from_excel(record.row, record.col, true, true),
8610                        );
8611                        let ingested = self
8612                            .graph
8613                            .ingest_pipeline(&snapshot)
8614                            .enable_function_semantics()
8615                            .ingest_formula(
8616                                FormulaAstInput::RawArena(record.ast_id),
8617                                placement,
8618                                record.formula_text,
8619                            )?;
8620                        if ingested.dep_plan.dynamic {
8621                            if proven_sheet_local_dynamic {
8622                                Self::widen_target_preparation_to_sheet(
8623                                    options.opaque_policy,
8624                                    &mut scope,
8625                                    &mut reasons,
8626                                    OpaqueReason::DynamicReference,
8627                                    &region.sheet,
8628                                )?;
8629                            } else {
8630                                Self::widen_target_preparation(
8631                                    options.opaque_policy,
8632                                    &mut scope,
8633                                    &mut reasons,
8634                                    OpaqueReason::DynamicReference,
8635                                )?;
8636                            }
8637                        }
8638                        for dep in &ingested.dep_plan.direct_cell_deps {
8639                            self.target_preparation_checkpoint(options.deadline, 1)?;
8640                            regions.push_back(PreparationRegion {
8641                                sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
8642                                sheet_id: dep.sheet_id,
8643                                start_row: dep.coord.row().saturating_add(1),
8644                                start_col: dep.coord.col().saturating_add(1),
8645                                end_row: dep.coord.row().saturating_add(1),
8646                                end_col: dep.coord.col().saturating_add(1),
8647                            });
8648                        }
8649                        for range in &ingested.dep_plan.range_deps {
8650                            self.target_preparation_checkpoint(options.deadline, 1)?;
8651                            // `Current` is the sheet the staged package's formula
8652                            // lives on.
8653                            let Ok(dependency_sheet) =
8654                                self.resolve_sheet_locator(&range.sheet, package.sheet_id)
8655                            else {
8656                                Self::widen_target_preparation(
8657                                    options.opaque_policy,
8658                                    &mut scope,
8659                                    &mut reasons,
8660                                    OpaqueReason::UnresolvedCrossSheetBinding,
8661                                )?;
8662                                continue;
8663                            };
8664                            regions.push_back(PreparationRegion {
8665                                sheet: self.graph.sheet_name(dependency_sheet).to_string(),
8666                                sheet_id: dependency_sheet,
8667                                start_row: range.start_row.map_or(1, |bound| bound.index + 1),
8668                                start_col: range.start_col.map_or(1, |bound| bound.index + 1),
8669                                end_row: range
8670                                    .end_row
8671                                    .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
8672                                        bound.index + 1
8673                                    }),
8674                                end_col: range
8675                                    .end_col
8676                                    .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
8677                                        bound.index + 1
8678                                    }),
8679                            });
8680                        }
8681                        for name in ingested
8682                            .dep_plan
8683                            .resolved_named_refs
8684                            .iter()
8685                            .chain(&ingested.dep_plan.named_refs)
8686                        {
8687                            self.target_preparation_checkpoint(options.deadline, 1)?;
8688                            if let Some(entry) =
8689                                self.graph.resolve_name_entry(name, package.sheet_id)
8690                            {
8691                                symbol_vertices.push_back(entry.vertex);
8692                            } else if self.graph.resolve_source_scalar_entry(name).is_none()
8693                                && self.graph.resolve_source_table_entry(name).is_none()
8694                            {
8695                                Self::widen_target_preparation(
8696                                    options.opaque_policy,
8697                                    &mut scope,
8698                                    &mut reasons,
8699                                    OpaqueReason::UnresolvedName,
8700                                )?;
8701                            }
8702                        }
8703                        for table in &ingested.dep_plan.table_refs {
8704                            self.target_preparation_checkpoint(options.deadline, 1)?;
8705                            if let Some(entry) = self.graph.resolve_table_entry(table) {
8706                                symbol_vertices.push_back(entry.vertex);
8707                            } else if self.graph.resolve_source_table_entry(table).is_none() {
8708                                Self::widen_target_preparation(
8709                                    options.opaque_policy,
8710                                    &mut scope,
8711                                    &mut reasons,
8712                                    OpaqueReason::UnresolvedTable,
8713                                )?;
8714                            }
8715                        }
8716                        package.legacy.push((
8717                            record.row,
8718                            record.col,
8719                            ingested.ast_id,
8720                            ingested.dep_plan,
8721                        ));
8722                    }
8723                    prepared_packages.push(package);
8724                }
8725            }
8726            let leases = self.staged_formula_index.leases_in_region(
8727                &region.sheet,
8728                region.start_row,
8729                region.start_col,
8730                region.end_row,
8731                region.end_col,
8732            );
8733            for lease in leases {
8734                self.target_preparation_checkpoint(options.deadline, 1)?;
8735                let sheet_id = self.graph.sheet_id(&region.sheet).ok_or_else(|| {
8736                    ExcelError::new(ExcelErrorKind::Ref)
8737                        .with_message(format!("staged formula sheet not found: {}", region.sheet))
8738                })?;
8739                let key = (region.sheet.clone(), lease.row, lease.col, lease.generation);
8740                if !selected.insert(key) {
8741                    continue;
8742                }
8743                let text = self
8744                    .staged_formulas
8745                    .get(&region.sheet)
8746                    .and_then(|sheet| sheet.get_ordinary(lease.row, lease.col))
8747                    .ok_or_else(|| {
8748                        ExcelError::new(ExcelErrorKind::Value)
8749                            .with_message("staged formula index is stale")
8750                    })?
8751                    .to_string();
8752                let formula = if text.starts_with('=') {
8753                    text.clone()
8754                } else {
8755                    format!("={text}")
8756                };
8757                self.target_preparation_checkpoint(options.deadline, 1)?;
8758                let ast = match formualizer_parse::parser::parse(&formula) {
8759                    Ok(ast) => ast,
8760                    Err(error) => {
8761                        if self.config.formula_parse_policy == FormulaParsePolicy::Strict {
8762                            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8763                                format!(
8764                                    "Formula parse error at {}!{}{}: {error}",
8765                                    region.sheet,
8766                                    col_letters_from_1based(lease.col)
8767                                        .unwrap_or_else(|_| "?".to_string()),
8768                                    lease.row
8769                                ),
8770                            ));
8771                        }
8772                        pending_diagnostics.push(FormulaParseDiagnostic {
8773                            sheet: region.sheet.clone(),
8774                            row: lease.row,
8775                            col: lease.col,
8776                            formula: formula.clone(),
8777                            message: error.to_string(),
8778                            policy: self.config.formula_parse_policy,
8779                        });
8780                        match self.config.formula_parse_policy {
8781                            FormulaParsePolicy::KeepCachedValue => {
8782                                selected_cells.push(
8783                                    formualizer_common::RangeAddress::new(
8784                                        region.sheet.clone(),
8785                                        lease.row,
8786                                        lease.col,
8787                                        lease.row,
8788                                        lease.col,
8789                                    )
8790                                    .expect("selected staged coordinates are valid"),
8791                                );
8792                                prepared.push(PreparedOrdinaryStagedFormula {
8793                                    sheet: region.sheet.clone(),
8794                                    sheet_id,
8795                                    lease,
8796                                    ast_id: None,
8797                                    plan: None,
8798                                });
8799                                continue;
8800                            }
8801                            FormulaParsePolicy::AsText => ASTNode::new(
8802                                ASTNodeType::Literal(LiteralValue::Text(formula.clone())),
8803                                None,
8804                            ),
8805                            FormulaParsePolicy::CoerceToError => ASTNode::new(
8806                                ASTNodeType::Literal(LiteralValue::Error(
8807                                    ExcelError::new(ExcelErrorKind::Error)
8808                                        .with_message(format!("Malformed formula: {error}")),
8809                                )),
8810                                None,
8811                            ),
8812                            FormulaParsePolicy::Strict => unreachable!(),
8813                        }
8814                    }
8815                };
8816                self.target_preparation_checkpoint(options.deadline, 1)?;
8817                let snapshot = self.target_planning_snapshot(&ast, &mut planning_requests)?;
8818                self.target_preparation_checkpoint(options.deadline, 1)?;
8819                if let Some(reason) =
8820                    Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8821                {
8822                    return Err(Self::preparation_stale(
8823                        reason,
8824                        "target planning snapshot became stale during discovery",
8825                    ));
8826                }
8827                if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
8828                    if reason == OpaqueReason::DynamicReference
8829                        && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
8830                    {
8831                        Self::widen_target_preparation_to_sheet(
8832                            options.opaque_policy,
8833                            &mut scope,
8834                            &mut reasons,
8835                            reason,
8836                            &region.sheet,
8837                        )?;
8838                    } else {
8839                        Self::widen_target_preparation(
8840                            options.opaque_policy,
8841                            &mut scope,
8842                            &mut reasons,
8843                            reason,
8844                        )?;
8845                    }
8846                }
8847                let proven_sheet_local_dynamic =
8848                    Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
8849                let placement = CellRef::new(
8850                    sheet_id,
8851                    Coord::from_excel(lease.row, lease.col, true, true),
8852                );
8853                let ingested = self.graph.ingest_pipeline(&snapshot).ingest_formula(
8854                    FormulaAstInput::Tree(ast),
8855                    placement,
8856                    Some(Arc::from(formula)),
8857                )?;
8858                self.target_preparation_checkpoint(options.deadline, 1)?;
8859                if ingested.dep_plan.dynamic {
8860                    if proven_sheet_local_dynamic {
8861                        Self::widen_target_preparation_to_sheet(
8862                            options.opaque_policy,
8863                            &mut scope,
8864                            &mut reasons,
8865                            OpaqueReason::DynamicReference,
8866                            &region.sheet,
8867                        )?;
8868                    } else {
8869                        Self::widen_target_preparation(
8870                            options.opaque_policy,
8871                            &mut scope,
8872                            &mut reasons,
8873                            OpaqueReason::DynamicReference,
8874                        )?;
8875                    }
8876                }
8877                for dep in &ingested.dep_plan.direct_cell_deps {
8878                    self.target_preparation_checkpoint(options.deadline, 1)?;
8879                    regions.push_back(PreparationRegion {
8880                        sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
8881                        sheet_id: dep.sheet_id,
8882                        start_row: dep.coord.row() + 1,
8883                        start_col: dep.coord.col() + 1,
8884                        end_row: dep.coord.row() + 1,
8885                        end_col: dep.coord.col() + 1,
8886                    });
8887                }
8888                for range in &ingested.dep_plan.range_deps {
8889                    self.target_preparation_checkpoint(options.deadline, 1)?;
8890                    // `Current` is the sheet the staged formula lives on.
8891                    let Ok(dependency_sheet) = self.resolve_sheet_locator(&range.sheet, sheet_id)
8892                    else {
8893                        Self::widen_target_preparation(
8894                            options.opaque_policy,
8895                            &mut scope,
8896                            &mut reasons,
8897                            OpaqueReason::UnresolvedCrossSheetBinding,
8898                        )?;
8899                        continue;
8900                    };
8901                    regions.push_back(PreparationRegion {
8902                        sheet: self.graph.sheet_name(dependency_sheet).to_string(),
8903                        sheet_id: dependency_sheet,
8904                        start_row: range.start_row.map_or(1, |bound| bound.index + 1),
8905                        start_col: range.start_col.map_or(1, |bound| bound.index + 1),
8906                        end_row: range
8907                            .end_row
8908                            .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
8909                                bound.index + 1
8910                            }),
8911                        end_col: range
8912                            .end_col
8913                            .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
8914                                bound.index + 1
8915                            }),
8916                    });
8917                }
8918                for name in ingested
8919                    .dep_plan
8920                    .resolved_named_refs
8921                    .iter()
8922                    .chain(&ingested.dep_plan.named_refs)
8923                {
8924                    self.target_preparation_checkpoint(options.deadline, 1)?;
8925                    if let Some(entry) = self.graph.resolve_name_entry(name, sheet_id) {
8926                        symbol_vertices.push_back(entry.vertex);
8927                    } else if self.graph.resolve_source_scalar_entry(name).is_none()
8928                        && self.graph.resolve_source_table_entry(name).is_none()
8929                    {
8930                        Self::widen_target_preparation(
8931                            options.opaque_policy,
8932                            &mut scope,
8933                            &mut reasons,
8934                            OpaqueReason::UnresolvedName,
8935                        )?;
8936                    }
8937                }
8938                for table in &ingested.dep_plan.table_refs {
8939                    self.target_preparation_checkpoint(options.deadline, 1)?;
8940                    if let Some(entry) = self.graph.resolve_table_entry(table) {
8941                        symbol_vertices.push_back(entry.vertex);
8942                    } else if self.graph.resolve_source_table_entry(table).is_none() {
8943                        Self::widen_target_preparation(
8944                            options.opaque_policy,
8945                            &mut scope,
8946                            &mut reasons,
8947                            OpaqueReason::UnresolvedTable,
8948                        )?;
8949                    }
8950                }
8951                selected_cells.push(
8952                    formualizer_common::RangeAddress::new(
8953                        region.sheet.clone(),
8954                        lease.row,
8955                        lease.col,
8956                        lease.row,
8957                        lease.col,
8958                    )
8959                    .expect("selected staged coordinates are valid"),
8960                );
8961                prepared.push(PreparedOrdinaryStagedFormula {
8962                    sheet: region.sheet.clone(),
8963                    sheet_id,
8964                    lease,
8965                    ast_id: Some(ingested.ast_id),
8966                    plan: Some(ingested.dep_plan),
8967                });
8968            }
8969
8970            if indexed_query_sheets.insert(region.sheet_id) {
8971                self.graph.prepare_sheet_index_for_query(region.sheet_id);
8972                let bytes = (self.graph.sheet_index_vertex_count(region.sheet_id) as u64)
8973                    .saturating_mul(32);
8974                self.reserve_graph_source_scratch(bytes)?;
8975                discovery_scratch_reserved = discovery_scratch_reserved.saturating_add(bytes);
8976            }
8977            let spill_anchors = self.graph.spill_anchors_in_region(
8978                region.sheet_id,
8979                region.start_row - 1,
8980                region.start_col - 1,
8981                region.end_row - 1,
8982                region.end_col - 1,
8983            );
8984            for anchor in spill_anchors {
8985                self.target_preparation_checkpoint(options.deadline, 1)?;
8986                symbol_vertices.push_back(anchor);
8987            }
8988            let vertices = self.graph.vertices_in_region(
8989                region.sheet_id,
8990                region.start_row - 1,
8991                region.end_row - 1,
8992                region.start_col - 1,
8993                region.end_col - 1,
8994            );
8995            for vertex in vertices {
8996                self.target_preparation_checkpoint(options.deadline, 1)?;
8997                symbol_vertices.push_back(vertex);
8998            }
8999        }
9000
9001        #[cfg(test)]
9002        self.target_preparation_fault(
9003            crate::engine::target_preparation::TargetPreparationFault::AfterDiscovery,
9004        )?;
9005
9006        if package_encountered {
9007            Self::widen_target_preparation(
9008                options.opaque_policy,
9009                &mut scope,
9010                &mut reasons,
9011                OpaqueReason::UnsupportedSourceSemantics,
9012            )?;
9013            self.target_preparation_checkpoint(options.deadline, 0)?;
9014            let selected_count = self.staged_formula_count();
9015            let selected_packages = self
9016                .staged_formulas
9017                .values()
9018                .filter_map(|staged| staged.deferred_package.as_ref())
9019                .map(|package| package.families.len() + package.partitioned_families.len())
9020                .sum();
9021            self.formula_parse_diagnostics.truncate(diagnostics_len);
9022            self.last_formula_ingest_report = report_len;
9023            #[cfg(test)]
9024            if let Some(hook) = self.before_target_preparation_commit_hook.take() {
9025                hook();
9026            }
9027            self.target_preparation_checkpoint(options.deadline, 0)?;
9028            #[cfg(test)]
9029            self.target_preparation_fault(
9030                crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
9031            )?;
9032            let current_revisions = self.preparation_revisions();
9033            if let Some(reason) = Self::preparation_revision_stale_reason(
9034                &assumptions,
9035                &current_revisions,
9036                &planning_requests,
9037                true,
9038            ) {
9039                return Err(Self::preparation_stale(
9040                    reason,
9041                    "target compatibility preparation plan is stale",
9042                ));
9043            }
9044            #[cfg(test)]
9045            self.target_preparation_fault(
9046                crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9047            )?;
9048            let commit_work_before = self
9049                .active_resource_ledger
9050                .as_ref()
9051                .map_or(0, |ledger| ledger.snapshot().work_charged);
9052            let commit_started = crate::instant::FzInstant::now();
9053            self.build_graph_all_unobserved()?;
9054            let commit_window = commit_started.elapsed();
9055            let ledger_after = self
9056                .active_resource_ledger
9057                .as_ref()
9058                .map(|ledger| ledger.snapshot());
9059            let actual_commit_work = ledger_after
9060                .map_or(0, |snapshot| snapshot.work_charged)
9061                .saturating_sub(commit_work_before);
9062            let observed_scratch_bytes = ledger_after
9063                .map_or(0, |snapshot| snapshot.scratch_peak)
9064                .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9065            let revisions = assumptions.clone();
9066            let report = PreparedTargetGraphReport {
9067                request_id: request_id.unwrap_or_default(),
9068                requested_targets: targets.len(),
9069                normalized_regions: visited_regions.len(),
9070                normalized_target_list: normalized,
9071                selected_staged_cells: selected_count,
9072                selected_source_families: selected_packages,
9073                retained_staged_cells: self.staged_formula_count(),
9074                selected_cells,
9075                retained_cells: Vec::new(),
9076                widened_scope: PrepareScope::Workbook,
9077                widening_reasons: reasons,
9078                revisions,
9079                commit_window,
9080                estimated_scratch_bytes: discovery_scratch_reserved
9081                    .saturating_add((selected_count as u64).saturating_mul(256)),
9082                observed_scratch_bytes,
9083                estimated_commit_work: selected_count as u64,
9084                actual_commit_work,
9085                outcome: PreparationOutcome::CompatibilityPrepared,
9086            };
9087            self.observe_target_preparation_report(&report);
9088            return Ok(report);
9089        }
9090
9091        prepared.sort_by_key(|formula| formula.lease.insertion_order);
9092        for package in &prepared_packages {
9093            if let Some(points) = &package.selected_points {
9094                selected_cells.extend(points.iter().filter_map(|&(row, col)| {
9095                    formualizer_common::RangeAddress::new(&package.sheet, row, col, row, col).ok()
9096                }));
9097                continue;
9098            }
9099            selected_cells.extend(package.replay_records.iter().filter_map(|record| {
9100                formualizer_common::RangeAddress::new(
9101                    package.sheet.clone(),
9102                    record.row,
9103                    record.col,
9104                    record.row,
9105                    record.col,
9106                )
9107                .ok()
9108            }));
9109        }
9110        let (legacy_graph, planned_formula_count) =
9111            self.prepare_target_combined_legacy_graph(&prepared_packages, &prepared)?;
9112        let new_vertices = legacy_graph.new_vertex_count();
9113        let new_edges = legacy_graph.planned_edge_count().ok_or_else(|| {
9114            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9115        })?;
9116        let removed_edges = legacy_graph.removed_edge_count().ok_or_else(|| {
9117            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9118        })?;
9119        let current = self.graph.baseline_stats();
9120        let final_vertices = current
9121            .graph_vertex_count
9122            .checked_add(new_vertices)
9123            .ok_or_else(|| {
9124                crate::engine::ResourceLedgerError::Exhausted(
9125                    formualizer_common::ResourceExhaustionDetail {
9126                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9127                        limit: u64::MAX,
9128                        observed: u64::MAX,
9129                        request_id,
9130                    },
9131                )
9132                .into_excel_error()
9133            })?;
9134        let final_edges = current
9135            .graph_edge_count
9136            .checked_sub(removed_edges)
9137            .and_then(|count| count.checked_add(new_edges))
9138            .ok_or_else(|| {
9139                crate::engine::ResourceLedgerError::Exhausted(
9140                    formualizer_common::ResourceExhaustionDetail {
9141                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9142                        limit: u64::MAX,
9143                        observed: u64::MAX,
9144                        request_id,
9145                    },
9146                )
9147                .into_excel_error()
9148            })?;
9149        #[cfg(test)]
9150        self.target_preparation_fault(
9151            crate::engine::target_preparation::TargetPreparationFault::Admission,
9152        )?;
9153        let resource = |reason, limit: u64, observed: u64| {
9154            crate::engine::ResourceLedgerError::Exhausted(
9155                formualizer_common::ResourceExhaustionDetail {
9156                    reason,
9157                    limit,
9158                    observed,
9159                    request_id,
9160                },
9161            )
9162            .into_excel_error()
9163        };
9164        let admission = crate::engine::resource_ledger::GraphAdmission {
9165            final_vertices,
9166            final_edges,
9167            materialization_cells: planned_formula_count as u64,
9168            added_vertices: new_vertices,
9169            added_edges: new_edges,
9170        };
9171        let materialized_bytes = admission
9172            .materialized_graph_bytes()
9173            .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
9174        if let Err(error) = self.preflight_graph_admission(admission) {
9175            if let formualizer_common::ExcelErrorExtra::Resource { detail } = &error.extra {
9176                self.observe_target_admission_failure(detail.reason);
9177            }
9178            return Err(error);
9179        }
9180        let selected_package_records = prepared_packages
9181            .iter()
9182            .map(|package| package.replay_records.len() as u64)
9183            .sum::<u64>();
9184        let planned_working_bytes = (prepared.len() as u64)
9185            .saturating_add(selected_package_records)
9186            .saturating_mul(256)
9187            .saturating_add((visited_regions.len() as u64).saturating_mul(128))
9188            .saturating_add((visited_vertices.len() as u64).saturating_mul(32))
9189            .saturating_add((selected.len() as u64).saturating_mul(96))
9190            .saturating_add(materialized_bytes);
9191        let residual_scratch = selected_package_points
9192            .iter()
9193            .map(|(sheet, points)| {
9194                let source = self
9195                    .staged_formulas
9196                    .get(sheet)
9197                    .unwrap()
9198                    .deferred_package
9199                    .as_ref()
9200                    .unwrap();
9201                (source.families.len() as u64)
9202                    .saturating_mul(256)
9203                    .saturating_add(
9204                        source
9205                            .partitioned_families
9206                            .iter()
9207                            .map(|family| {
9208                                256u64
9209                                    .saturating_add(family.fragments.len() as u64 * 32)
9210                                    .saturating_add(family.legacy_members.len() as u64 * 32)
9211                            })
9212                            .sum::<u64>(),
9213                    )
9214                    // Direct complete domains need coordinate suppression, not
9215                    // a per-member residual split/proof or legacy AST reserve.
9216                    .saturating_add(points.len() as u64 * 32)
9217                    .saturating_add(
9218                        prepared_packages
9219                            .iter()
9220                            .filter(|package| &package.sheet == sheet)
9221                            .map(|package| package.replay_records.len() as u64 * 480)
9222                            .sum::<u64>(),
9223                    )
9224            })
9225            .sum::<u64>();
9226        let scratch_bytes = discovery_scratch_reserved
9227            .saturating_add(planned_working_bytes)
9228            .saturating_add(residual_scratch);
9229        let remaining_scratch = scratch_bytes.saturating_sub(discovery_scratch_reserved);
9230        if let Err(error) = self.reserve_graph_source_scratch(remaining_scratch) {
9231            self.observe_target_admission_failure(
9232                formualizer_common::ResourceExhaustionReason::ScratchMemory,
9233            );
9234            return Err(error);
9235        }
9236
9237        let mut residual_sources = BTreeMap::new();
9238        let mut residual_owners = BTreeMap::new();
9239        for (sheet, points) in &selected_package_points {
9240            let selected: BTreeMap<_, BTreeSet<_>> = prepared_packages
9241                .iter()
9242                .filter(|package| &package.sheet == sheet)
9243                .flat_map(|package| &package.replay_records)
9244                .filter_map(|record| {
9245                    record.partition_owner.or(record.family).map(|owner| {
9246                        (
9247                            owner,
9248                            crate::engine::SourceCoord {
9249                                row: record.row - 1,
9250                                col: record.col - 1,
9251                            },
9252                        )
9253                    })
9254                })
9255                .fold(BTreeMap::new(), |mut map, (owner, coord)| {
9256                    map.entry(owner).or_default().insert(coord);
9257                    map
9258                });
9259            let metadata_work = self
9260                .staged_formulas
9261                .get(sheet)
9262                .unwrap()
9263                .deferred_package
9264                .as_ref()
9265                .map_or(0, |source| {
9266                    source.families.len() as u64
9267                        + source
9268                            .partitioned_families
9269                            .iter()
9270                            .map(|family| {
9271                                1 + family.fragments.len() as u64 * family.fragments.len() as u64
9272                                    + family.legacy_members.len() as u64
9273                            })
9274                            .sum::<u64>()
9275                });
9276            let split_work = selected
9277                .values()
9278                .map(|points| points.len() as u64 * 128)
9279                .sum::<u64>();
9280            self.target_preparation_checkpoint(
9281                options.deadline,
9282                metadata_work
9283                    .saturating_add(split_work)
9284                    .saturating_add(points.len() as u64),
9285            )?;
9286            let source = self
9287                .staged_formulas
9288                .get(sheet)
9289                .unwrap()
9290                .deferred_package
9291                .as_ref()
9292                .unwrap();
9293            let complete: BTreeSet<_> = prepared_packages
9294                .iter()
9295                .filter(|package| &package.sheet == sheet)
9296                .flat_map(|package| package.complete_selections.iter().copied())
9297                .collect();
9298            let residual = source
9299                .residual_sources(&selected, &complete, &self.workbook_load_limits)
9300                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
9301            residual_owners.insert(
9302                sheet.clone(),
9303                residual
9304                    .1
9305                    .iter()
9306                    .map(|family| family.source_id)
9307                    .collect::<BTreeSet<_>>(),
9308            );
9309            residual_sources.insert(sheet.clone(), residual);
9310        }
9311
9312        // Reserve residual suppression before the revision-validated commit window.
9313        // Hash-set insertion and point-index removal below cannot allocate.
9314        for (sheet, points) in &selected_package_points {
9315            let package = self
9316                .staged_formulas
9317                .get_mut(sheet)
9318                .unwrap()
9319                .deferred_package
9320                .as_mut()
9321                .unwrap();
9322            let owner_count = prepared_packages
9323                .iter()
9324                .filter(|p| &p.sheet == sheet)
9325                .flat_map(|p| &p.replay_records)
9326                .filter_map(|record| record.partition_owner.or(record.family))
9327                .filter(|owner| residual_owners[sheet].contains(owner))
9328                .count();
9329            package
9330                .consumed_members
9331                .try_reserve(owner_count)
9332                .map_err(|_| {
9333                    resource(
9334                        formualizer_common::ResourceExhaustionReason::ScratchMemory,
9335                        0,
9336                        owner_count as u64 * 24,
9337                    )
9338                })?;
9339            package.suppressed.try_reserve(points.len()).map_err(|_| {
9340                resource(
9341                    formualizer_common::ResourceExhaustionReason::ScratchMemory,
9342                    0,
9343                    points.len() as u64 * 16,
9344                )
9345            })?;
9346        }
9347
9348        let estimated_commit_duration = std::time::Duration::from_nanos(
9349            (new_vertices as u64)
9350                .saturating_add(new_edges as u64)
9351                .saturating_add(prepared.len() as u64)
9352                .saturating_add(selected_package_records)
9353                .max(1)
9354                .saturating_mul(100),
9355        );
9356        if options.deadline.is_some_and(|deadline| {
9357            std::time::Instant::now()
9358                .checked_add(estimated_commit_duration)
9359                .is_none_or(|finish| finish > deadline)
9360        }) {
9361            self.observe_target_admission_failure(
9362                formualizer_common::ResourceExhaustionReason::Deadline,
9363            );
9364            return Err(resource(
9365                formualizer_common::ResourceExhaustionReason::Deadline,
9366                0,
9367                1,
9368            ));
9369        }
9370        #[cfg(test)]
9371        if let Some(hook) = self.before_target_preparation_commit_hook.take() {
9372            hook();
9373        }
9374        self.target_preparation_checkpoint(options.deadline, 0)?;
9375        #[cfg(test)]
9376        self.target_preparation_fault(
9377            crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
9378        )?;
9379        let current_revisions = self.preparation_revisions();
9380        let staged_leases_match = prepared.iter().all(|formula| {
9381            self.staged_formula_index
9382                .lease_matches(&formula.sheet, formula.lease)
9383        }) && prepared_packages.iter().all(|package| {
9384            self.staged_formula_index
9385                .package_lease_matches(&package.sheet, package.lease)
9386        });
9387        let stale_reason = Self::preparation_revision_stale_reason(
9388            &assumptions,
9389            &current_revisions,
9390            &planning_requests,
9391            staged_leases_match,
9392        );
9393        if let Some(reason) = stale_reason {
9394            return Err(Self::preparation_stale(
9395                reason,
9396                "target graph preparation plan is stale",
9397            ));
9398        }
9399        #[cfg(test)]
9400        self.target_preparation_fault(
9401            crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9402        )?;
9403        self.graph
9404            .validate_prepared_legacy_graph_plan(&legacy_graph)
9405            .map_err(|error| {
9406                Self::preparation_stale(
9407                    formualizer_common::PreparationStaleReason::Graph,
9408                    format!("target graph preparation plan is stale: {error}"),
9409                )
9410            })?;
9411        #[cfg(test)]
9412        self.target_preparation_fault(
9413            crate::engine::target_preparation::TargetPreparationFault::Reservation,
9414        )?;
9415        self.graph.reserve_prepared_legacy_graph_plan(&legacy_graph);
9416        self.formula_parse_diagnostics
9417            .try_reserve(pending_diagnostics.len())
9418            .map_err(|_| {
9419                resource(
9420                    formualizer_common::ResourceExhaustionReason::Admission,
9421                    pending_diagnostics.len() as u64,
9422                    pending_diagnostics.len() as u64,
9423                )
9424            })?;
9425        self.target_preparation_checkpoint(options.deadline, 0)?;
9426        #[cfg(test)]
9427        self.target_preparation_fault(
9428            crate::engine::target_preparation::TargetPreparationFault::BeforeFirstMutation,
9429        )?;
9430
9431        let commit_started = crate::instant::FzInstant::now();
9432        let committed = self
9433            .graph
9434            .apply_prevalidated_legacy_graph_plan(legacy_graph);
9435        for formula in &prepared {
9436            let removed = self
9437                .staged_formulas
9438                .get_mut(&formula.sheet)
9439                .and_then(|sheet| sheet.remove_ordinary(formula.lease.row, formula.lease.col));
9440            debug_assert!(removed.is_some());
9441            let index_removed = self.staged_formula_index.remove(
9442                &formula.sheet,
9443                formula.lease.row,
9444                formula.lease.col,
9445            );
9446            debug_assert!(index_removed);
9447        }
9448        for package in &prepared_packages {
9449            if let Some(points) = &package.selected_points {
9450                let staged = self.staged_formulas.get_mut(&package.sheet).unwrap();
9451                let source = staged.deferred_package.as_mut().unwrap();
9452                source.consumed_engine = Some(Arc::clone(&self.source_formula_token));
9453                // The revision-validated graph commit establishes this exact source
9454                // ownership proof. Later edits retain the exclusion, not old text.
9455                source
9456                    .consumed_members
9457                    .extend(package.replay_records.iter().filter_map(|record| {
9458                        record
9459                            .partition_owner
9460                            .or(record.family)
9461                            .filter(|owner| residual_owners[&package.sheet].contains(owner))
9462                            .map(|owner| {
9463                                (
9464                                    owner,
9465                                    crate::engine::SourceCoord {
9466                                        row: record.row - 1,
9467                                        col: record.col - 1,
9468                                    },
9469                                )
9470                            })
9471                    }));
9472                source.suppressed.extend(points.iter().copied());
9473                source.source_accounted = true;
9474                self.staged_formula_index
9475                    .consume_package_points(&package.sheet, points);
9476                if source.suppressed.len() >= source.source_coordinates.len() {
9477                    staged.deferred_package = None;
9478                    self.staged_formula_index.set_package(&package.sheet, None);
9479                }
9480            } else {
9481                let removed = self
9482                    .staged_formulas
9483                    .get_mut(&package.sheet)
9484                    .and_then(|staged| staged.deferred_package.take());
9485                debug_assert!(removed.is_some());
9486                self.staged_formula_index.set_package(&package.sheet, None);
9487            }
9488        }
9489        for (sheet, (families, partitions)) in residual_sources {
9490            if let Some(source) = self
9491                .staged_formulas
9492                .get_mut(&sheet)
9493                .and_then(|staged| staged.deferred_package.as_mut())
9494            {
9495                source.families = families;
9496                source.partitioned_families = partitions;
9497                source
9498                    .consumed_members
9499                    .retain(|(owner, _)| residual_owners[&sheet].contains(owner));
9500                self.staged_formula_index.update_package_family_count(
9501                    &sheet,
9502                    source.families.len() + source.partitioned_families.len(),
9503                );
9504            }
9505        }
9506        let empty_sheets = self
9507            .staged_formulas
9508            .iter()
9509            .filter_map(|(sheet, staged)| staged.is_empty().then_some(sheet.clone()))
9510            .collect::<Vec<_>>();
9511        for sheet in empty_sheets {
9512            self.staged_formulas.remove(&sheet);
9513        }
9514        if committed > 0 {
9515            self.mark_topology_edited();
9516        }
9517        self.formula_parse_diagnostics.extend(pending_diagnostics);
9518        if !prepared.is_empty() || !prepared_packages.is_empty() {
9519            let mut ingest_delta = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
9520            ingest_delta.formula_cells_seen = (prepared.len() as u64).saturating_add(
9521                prepared_packages
9522                    .iter()
9523                    .map(|package| {
9524                        (package.replay_records.len() as u64).saturating_add(
9525                            if package.selected_points.is_some() {
9526                                package.direct_cells
9527                            } else {
9528                                0
9529                            },
9530                        )
9531                    })
9532                    .sum::<u64>(),
9533            );
9534            ingest_delta.graph_formula_cells_materialized = committed as u64;
9535            ingest_delta.graph_vertices_created = new_vertices as u64;
9536            ingest_delta.graph_edges_created = new_edges as u64;
9537            for package in &prepared_packages {
9538                let source = &package.source_report;
9539                ingest_delta.source_formula_events = ingest_delta
9540                    .source_formula_events
9541                    .saturating_add(source.source_formula_events);
9542                ingest_delta.source_formula_records_spooled = ingest_delta
9543                    .source_formula_records_spooled
9544                    .saturating_add(source.source_formula_records_spooled);
9545                ingest_delta.source_spool_encoded_bytes = ingest_delta
9546                    .source_spool_encoded_bytes
9547                    .saturating_add(source.source_spool_encoded_bytes);
9548                ingest_delta.source_spool_peak_memory_bytes = ingest_delta
9549                    .source_spool_peak_memory_bytes
9550                    .max(source.source_spool_peak_memory_bytes);
9551                ingest_delta.source_spool_spilled_bytes = ingest_delta
9552                    .source_spool_spilled_bytes
9553                    .saturating_add(source.source_spool_spilled_bytes);
9554                ingest_delta.source_spool_spill_files = ingest_delta
9555                    .source_spool_spill_files
9556                    .saturating_add(source.source_spool_spill_files);
9557                ingest_delta.source_spool_replays = ingest_delta
9558                    .source_spool_replays
9559                    .saturating_add(source.source_spool_replays)
9560                    .saturating_add(package.spool_replays);
9561                ingest_delta.source_families_seen = ingest_delta
9562                    .source_families_seen
9563                    .saturating_add(source.families_seen);
9564                ingest_delta.source_family_cells_seen = ingest_delta
9565                    .source_family_cells_seen
9566                    .saturating_add(source.family_cells_seen);
9567                ingest_delta.source_family_shadow_eligible = ingest_delta
9568                    .source_family_shadow_eligible
9569                    .saturating_add(source.source_clean_families);
9570                ingest_delta.source_family_shadow_eligible_cells = ingest_delta
9571                    .source_family_shadow_eligible_cells
9572                    .saturating_add(source.source_clean_cells);
9573                ingest_delta.source_partitioned_families_seen = ingest_delta
9574                    .source_partitioned_families_seen
9575                    .saturating_add(source.source_fragmentable_families);
9576                ingest_delta.source_partition_holes = ingest_delta
9577                    .source_partition_holes
9578                    .saturating_add(source.source_hole_exclusions);
9579                ingest_delta.source_partition_ordinary_exceptions = ingest_delta
9580                    .source_partition_ordinary_exceptions
9581                    .saturating_add(source.source_ordinary_exclusions);
9582                ingest_delta.source_partition_surviving_cells = ingest_delta
9583                    .source_partition_surviving_cells
9584                    .saturating_add(source.source_fragmentable_cells);
9585                for (reason, count) in &source.fallback_reasons {
9586                    let total = ingest_delta
9587                        .fallback_reasons
9588                        .entry(reason.clone())
9589                        .or_default();
9590                    *total = total.saturating_add(*count);
9591                }
9592
9593                ingest_delta.source_family_fallback = ingest_delta
9594                    .source_family_fallback
9595                    .saturating_add(source.families_seen);
9596                ingest_delta.source_family_fallback_cells = ingest_delta
9597                    .source_family_fallback_cells
9598                    .saturating_add(source.family_cells_seen);
9599            }
9600            self.record_formula_ingest_report(ingest_delta);
9601        }
9602        let commit_window = commit_started.elapsed();
9603        let retained_cells = self
9604            .staged_formula_index
9605            .all_leases()
9606            .into_iter()
9607            .filter_map(|(sheet, lease)| {
9608                formualizer_common::RangeAddress::new(
9609                    sheet, lease.row, lease.col, lease.row, lease.col,
9610                )
9611                .ok()
9612            })
9613            .collect::<Vec<_>>();
9614        let observed_scratch_bytes = self
9615            .active_resource_ledger
9616            .as_ref()
9617            .map_or(0, |ledger| ledger.snapshot().scratch_peak)
9618            .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9619        let committed_spans = 0u64;
9620        let selected_source_families = prepared_packages
9621            .iter()
9622            .filter(|package| package.selected_points.is_none())
9623            .map(|package| package.lease.family_count)
9624            .sum::<usize>()
9625            + prepared_packages
9626                .iter()
9627                .filter(|package| package.selected_points.is_some())
9628                .flat_map(|package| package.replay_records.iter())
9629                .filter_map(|record| record.partition_owner.or(record.family))
9630                .chain(
9631                    prepared_packages
9632                        .iter()
9633                        .flat_map(|package| package.complete_selections.iter().copied()),
9634                )
9635                .collect::<BTreeSet<_>>()
9636                .len();
9637        let selected_staged_cells = prepared.len().saturating_add(
9638            prepared_packages
9639                .iter()
9640                .map(|package| {
9641                    package
9642                        .selected_points
9643                        .as_ref()
9644                        .map_or(package.replay_records.len(), BTreeSet::len)
9645                })
9646                .sum::<usize>(),
9647        );
9648        let actual_commit_work = (new_vertices as u64)
9649            .saturating_add(new_edges as u64)
9650            .saturating_add(committed as u64)
9651            .saturating_add(committed_spans)
9652            .saturating_add(prepared.len() as u64)
9653            .saturating_add(
9654                prepared_packages
9655                    .iter()
9656                    .map(|package| {
9657                        package
9658                            .selected_points
9659                            .as_ref()
9660                            .map_or(1, |points| points.len() as u64)
9661                    })
9662                    .sum::<u64>(),
9663            );
9664        let report = PreparedTargetGraphReport {
9665            request_id: request_id.unwrap_or_default(),
9666            requested_targets: targets.len(),
9667            normalized_regions: visited_regions.len(),
9668            normalized_target_list: normalized,
9669            selected_staged_cells,
9670            selected_source_families,
9671            retained_staged_cells: self.staged_formula_count(),
9672            selected_cells,
9673            retained_cells,
9674            widened_scope: scope,
9675            widening_reasons: reasons,
9676            revisions: assumptions,
9677            commit_window,
9678            estimated_scratch_bytes: scratch_bytes,
9679            observed_scratch_bytes,
9680            estimated_commit_work: (new_vertices as u64)
9681                .saturating_add(new_edges as u64)
9682                .saturating_add(prepared.len() as u64)
9683                .saturating_add(selected_package_records),
9684            actual_commit_work,
9685            outcome: PreparationOutcome::Prepared,
9686        };
9687        self.observe_target_preparation_report(&report);
9688        Ok(report)
9689    }
9690
9691    /// Build graph for all staged formulas.
9692    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9693        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
9694            engine.build_graph_all_unobserved()
9695        })
9696    }
9697
9698    fn build_graph_all_unobserved(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9699        let selected = self.staged_formula_count();
9700        let started = crate::instant::FzInstant::now();
9701        self.resource_checkpoint(selected as u64)?;
9702        let scratch_bytes = (selected as u64).saturating_mul(256);
9703        let result = self.with_request_scratch(scratch_bytes, |engine| {
9704            let index_snapshot = engine.staged_formula_index.clone();
9705            let collected = std::mem::take(&mut engine.staged_formulas)
9706                .into_iter()
9707                .collect();
9708            engine.staged_formula_index.clear_all();
9709            engine.build_graph_from_staged_batches(collected, false, index_snapshot)
9710        });
9711        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9712        result
9713    }
9714
9715    /// Build graph for specific sheets (consuming only those staged entries).
9716    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
9717        &mut self,
9718        sheets: I,
9719    ) -> Result<(), formualizer_parse::ExcelError> {
9720        let mut sheets = sheets.into_iter();
9721        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, move |engine| {
9722            let name_scratch = (sheets.size_hint().0 as u64).saturating_mul(64);
9723            engine.with_request_scratch(name_scratch, |engine| {
9724                // Allocation failure follows the baseline process-fatal policy; it is not a
9725                // recoverable resource error or a new staged-preparation route.
9726                let names = sheets.by_ref().map(str::to_string).collect::<Vec<_>>();
9727                engine.charge_bounded_work(names.len() as u64)?;
9728                engine.build_graph_for_sheet_names_unobserved(names)
9729            })
9730        })
9731    }
9732
9733    fn build_graph_for_sheet_names_unobserved(
9734        &mut self,
9735        sheets: Vec<String>,
9736    ) -> Result<(), formualizer_parse::ExcelError> {
9737        let started = crate::instant::FzInstant::now();
9738        let selected = sheets
9739            .iter()
9740            .filter_map(|sheet| self.staged_formulas.get(sheet))
9741            .map(StagedSheet::len)
9742            .sum::<usize>();
9743        self.resource_checkpoint(selected as u64)?;
9744        let scratch_bytes = (selected as u64).saturating_mul(256);
9745        self.reserve_request_scratch(scratch_bytes)?;
9746        let index_snapshot = self.staged_formula_index.clone();
9747        let mut collected = Vec::new();
9748        for sheet in sheets {
9749            if let Some(staged) = self.staged_formulas.remove(&sheet) {
9750                self.index_removed_staged_sheet(&sheet, &staged);
9751                collected.push((sheet, staged));
9752            }
9753        }
9754        let result = self.build_graph_from_staged_batches(collected, true, index_snapshot);
9755        self.release_request_scratch(scratch_bytes);
9756        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9757        result
9758    }
9759
9760    fn build_graph_from_staged_batches(
9761        &mut self,
9762        collected: StagedFormulaBatches,
9763        share_parse_cache_across_sheets: bool,
9764        staged_index_snapshot: StagedFormulaIndex,
9765    ) -> Result<(), formualizer_parse::ExcelError> {
9766        if collected.is_empty() {
9767            return Ok(());
9768        }
9769        for (sheet, _) in &collected {
9770            let _ = self.add_sheet(sheet);
9771        }
9772
9773        let diagnostics_len = self.formula_parse_diagnostics.len();
9774        let mut collected = collected;
9775        let prepared = match self
9776            .prepare_staged_formula_batches(&mut collected, share_parse_cache_across_sheets)
9777        {
9778            Ok(prepared) => prepared,
9779            Err(error) => {
9780                self.formula_parse_diagnostics.truncate(diagnostics_len);
9781                for (sheet, staged) in collected {
9782                    self.restore_staged_sheet(sheet, staged);
9783                }
9784                self.staged_formula_index = staged_index_snapshot;
9785                return Err(error);
9786            }
9787        };
9788        let (ordinary, compressed, direct, may_fail) = prepared;
9789
9790        // A first build (no formula in the graph yet) goes through the eager
9791        // first load's machinery: the builder pre-allocates each column's
9792        // targets as one id run, installs family members as virtual runs and
9793        // builds the authority once at the end (decision 26).
9794        // The first load's builder plans and applies one chunk at a time, so
9795        // a planning error would leave earlier chunks in the graph; the
9796        // incremental path plans everything first. Check every distinct
9797        // formula first, in the incremental path's order (it reports the
9798        // same first error and leaves the graph untouched).
9799        let first_build = if self.deferred_build_can_be_first_load() {
9800            if let Err(error) = self.check_staged_formula_plans(
9801                ordinary
9802                    .iter()
9803                    .chain(compressed.iter().map(|(batch, _)| batch)),
9804                &may_fail,
9805            ) {
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.deferred_build_as_first_load()
9814        } else {
9815            None
9816        };
9817        let built_sheets: Vec<String> = if first_build.is_some() {
9818            collected.iter().map(|(sheet, _)| sheet.clone()).collect()
9819        } else {
9820            Vec::new()
9821        };
9822
9823        // Keep the original source/spool alive through every fallible ingestion route.
9824        // Graph admission may have committed a prefix; replay replaces those placements
9825        // rather than treating their cached values as authoritative source.
9826        let result = (|| {
9827            if !ordinary.is_empty() {
9828                self.ingest_formula_batches(ordinary)?;
9829            }
9830            if !compressed.is_empty() {
9831                self.ingest_compressed_formula_source_batches(compressed)?;
9832            }
9833            if !direct.is_empty() {
9834                self.finish_compressed_formula_sources(direct)?;
9835            }
9836            Ok(())
9837        })();
9838        if let Some(saved) = first_build {
9839            self.leave_deferred_first_load(saved, &built_sheets);
9840        }
9841        if let Err(error) = result {
9842            self.formula_parse_diagnostics.truncate(diagnostics_len);
9843            for (sheet, staged) in collected {
9844                self.restore_staged_sheet(sheet, staged);
9845            }
9846            self.staged_formula_index = staged_index_snapshot;
9847            return Err(error);
9848        }
9849        self.dedup_formula_parse_diagnostics_since(diagnostics_len);
9850        Ok(())
9851    }
9852
9853    /// Enter the first-load ingest mode for a deferred build when the graph
9854    /// holds no formula yet and no load is in progress: the settings the
9855    /// calamine loader uses for its eager first load (lazy sheet index, no
9856    /// small-range expansion, first-load fast path). Returns the settings
9857    /// to restore, or `None` when the build takes the incremental path.
9858    fn deferred_build_can_be_first_load(&self) -> bool {
9859        !self.graph.first_load_assume_new()
9860            && !self.graph_admission_enabled()
9861            && self.graph.formula_vertex_count() == 0
9862    }
9863
9864    /// Plan each distinct formula of `batches` that may fail planning once
9865    /// (a member is planned through its template, staged before it) and
9866    /// drop the plans: the first planning error, in the order the
9867    /// incremental ingest meets it.
9868    fn check_staged_formula_plans<'b>(
9869        &mut self,
9870        batches: impl Iterator<Item = &'b FormulaIngestBatch>,
9871        may_fail: &FxHashSet<crate::engine::arena::AstNodeId>,
9872    ) -> Result<(), ExcelError> {
9873        if may_fail.is_empty() {
9874            return Ok(());
9875        }
9876        let mut seen: FxHashSet<(SheetId, crate::engine::arena::AstNodeId)> = FxHashSet::default();
9877        for batch in batches {
9878            let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
9879                ExcelError::new(ExcelErrorKind::Ref)
9880                    .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
9881            })?;
9882            let mut pipeline = self.ingest_pipeline();
9883            for record in &batch.formulas {
9884                if record.member_anchor.is_some()
9885                    || !may_fail.contains(&record.ast_id)
9886                    || !seen.insert((sheet_id, record.ast_id))
9887                {
9888                    continue;
9889                }
9890                let placement = CellRef::new(
9891                    sheet_id,
9892                    Coord::from_excel(record.row, record.col, true, true),
9893                );
9894                pipeline.ingest_formula(
9895                    FormulaAstInput::RawArena(record.ast_id),
9896                    placement,
9897                    None,
9898                )?;
9899            }
9900        }
9901        Ok(())
9902    }
9903
9904    fn deferred_build_as_first_load(&mut self) -> Option<(crate::engine::SheetIndexMode, usize)> {
9905        if !self.deferred_build_can_be_first_load() {
9906            return None;
9907        }
9908        let saved = (
9909            self.graph.get_config().sheet_index_mode,
9910            self.config.range_expansion_limit,
9911        );
9912        self.graph
9913            .set_sheet_index_mode(crate::engine::SheetIndexMode::Lazy);
9914        self.config.range_expansion_limit = 0;
9915        self.graph.set_first_load_assume_new(true);
9916        self.graph.reset_ensure_touched();
9917        Some(saved)
9918    }
9919
9920    /// Leave the first-load mode of [`Self::deferred_build_as_first_load`]:
9921    /// the authority is built once here, as at the end of an eager load.
9922    fn leave_deferred_first_load(
9923        &mut self,
9924        (index_mode, range_limit): (crate::engine::SheetIndexMode, usize),
9925        sheets: &[String],
9926    ) {
9927        self.graph.set_first_load_assume_new(false);
9928        self.graph.reset_ensure_touched();
9929        self.graph.set_sheet_index_mode(index_mode);
9930        self.config.range_expansion_limit = range_limit;
9931        for sheet in sheets {
9932            self.graph.finalize_sheet_index(sheet);
9933        }
9934    }
9935
9936    fn prepare_staged_formula_batches(
9937        &mut self,
9938        collected: &mut StagedFormulaBatches,
9939        share_parse_cache_across_sheets: bool,
9940    ) -> Result<PreparedStagedFormulaBatches, formualizer_parse::ExcelError> {
9941        let mut ordinary = Vec::new();
9942        let mut compressed = Vec::new();
9943        let mut direct = Vec::new();
9944        let mut may_fail = FxHashSet::default();
9945        let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
9946            rustc_hash::FxHashMap::default();
9947        cache.reserve(4096);
9948
9949        for (sheet, staged) in collected {
9950            if !share_parse_cache_across_sheets {
9951                cache.clear();
9952            }
9953            // Load-time family grouping, as the eager first load does:
9954            // relative copies of the formula above (or to the left) become
9955            // members of its family and are never interned.
9956            let mut grouper = crate::engine::FormulaFamilyGrouper::new();
9957            // The staged texts, then the deferred package's replayed ones,
9958            // are walked once (no combined copy: a replay can be the whole
9959            // sheet).
9960            let mut replayed_records = Vec::new();
9961            let deferred_source = None;
9962            let mut deferred_fallback = None;
9963            if let Some(package) = staged.deferred_package.as_mut() {
9964                if package.sheet_name != *sheet {
9965                    return Err(ExcelError::new(ExcelErrorKind::Value)
9966                        .with_message("deferred formula package sheet mismatch"));
9967                }
9968                let eligible: Vec<_> = package
9969                    .families
9970                    .iter()
9971                    .filter(|family| !package.invalidated.contains(&family.source_id))
9972                    .cloned()
9973                    .collect();
9974                let eligible_partitions: Vec<_> = package
9975                    .partitioned_families
9976                    .iter()
9977                    .filter(|family| !package.invalidated.contains(&family.source_id))
9978                    .cloned()
9979                    .collect();
9980
9981                let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
9982                for partition in &eligible_partitions {
9983                    replay_disposition
9984                        .register_partition(partition, false)
9985                        .map_err(|reason| {
9986                            ExcelError::new(ExcelErrorKind::Value).with_message(reason)
9987                        })?;
9988                }
9989                replay_disposition
9990                    .extend_suppressed_excel_coords(package.suppressed.iter().copied());
9991                let replayed = package
9992                    .replay
9993                    .lock()
9994                    .map_err(|_| {
9995                        ExcelError::new(ExcelErrorKind::Value)
9996                            .with_message("deferred formula spool lock poisoned")
9997                    })?
9998                    .replay(&replay_disposition)
9999                    .map_err(|message| {
10000                        ExcelError::new(ExcelErrorKind::Value).with_message(message)
10001                    })?;
10002                replayed_records = replayed;
10003                let mut report = package.accounting_report();
10004                report.source_spool_replays = report.source_spool_replays.saturating_add(1);
10005                deferred_fallback = Some((report, package.families.clone(), eligible_partitions));
10006            }
10007
10008            let n_entries = staged.entries.len() + replayed_records.len();
10009            let entries = staged
10010                .entries
10011                .iter()
10012                .cloned()
10013                .map(|(row, col, text)| (row, col, text, None))
10014                .chain(replayed_records.into_iter().map(|record| {
10015                    (
10016                        record.row,
10017                        record.col,
10018                        record.text,
10019                        Some((record.source_order, record.family, record.partition_owner)),
10020                    )
10021                }));
10022            let mut formulas = Vec::with_capacity(n_entries);
10023            let staged_order_base = u64::MAX.saturating_sub(n_entries as u64);
10024            for (entry_index, (row, col, txt, source_proof)) in entries.enumerate() {
10025                let key = if txt.starts_with('=') {
10026                    txt
10027                } else {
10028                    format!("={txt}")
10029                };
10030                let staged_record = if let Some(cached) = cache.get(&key) {
10031                    cached.map(|ast_id| {
10032                        self.note_staged_formula(&mut grouper, row, col, ast_id);
10033                        FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(key)))
10034                    })
10035                } else {
10036                    let parsed = match formualizer_parse::parser::parse(&key) {
10037                        Ok(parsed) => Some(parsed),
10038                        Err(error) => self.handle_formula_parse_error(
10039                            sheet,
10040                            row,
10041                            col,
10042                            &key,
10043                            error.to_string(),
10044                        )?,
10045                    };
10046                    match parsed {
10047                        Some(ast) => {
10048                            let record = self.stage_formula_ast(&mut grouper, row, col, &ast, None);
10049                            if !record.is_family_member() && self.formula_may_fail_planning(&ast) {
10050                                may_fail.insert(record.ast_id);
10051                            }
10052                            // A member's text is not worth caching: relative
10053                            // copies do not repeat their text.
10054                            if record.is_family_member() {
10055                                Some(record)
10056                            } else {
10057                                let ast_id = record.ast_id;
10058                                cache.insert(key.clone(), Some(ast_id));
10059                                Some(FormulaIngestRecord::new(
10060                                    row,
10061                                    col,
10062                                    ast_id,
10063                                    Some(Arc::<str>::from(key)),
10064                                ))
10065                            }
10066                        }
10067                        None => {
10068                            cache.insert(key, None);
10069                            None
10070                        }
10071                    }
10072                };
10073
10074                if let Some(mut formula) = staged_record {
10075                    if let Some((order, family, owner)) = source_proof {
10076                        formula = formula.with_source_proof(order, family, owner);
10077                    } else if deferred_source.is_some() {
10078                        formula = formula.with_source_proof(
10079                            crate::engine::SourceFormulaOrder::new(
10080                                staged_order_base.saturating_add(entry_index as u64),
10081                            ),
10082                            None,
10083                            None,
10084                        );
10085                    }
10086                    formulas.push(formula);
10087                }
10088            }
10089
10090            let batch = FormulaIngestBatch::new(sheet.clone(), formulas);
10091            if let Some((report, preparation)) = deferred_source {
10092                direct.push((batch, report, preparation));
10093            } else if let Some((report, families, partitions)) = deferred_fallback {
10094                let source_batch = crate::engine::FormulaCompressedSourceBatch::with_proposals(
10095                    batch.sheet_name.clone(),
10096                    report,
10097                    families,
10098                    partitions,
10099                );
10100                compressed.push((batch, source_batch));
10101            } else if !batch.is_empty() {
10102                ordinary.push(batch);
10103            }
10104        }
10105        Ok((ordinary, compressed, direct, may_fail))
10106    }
10107
10108    /// Whether planning `ast` can fail: a reference to a sheet that does not
10109    /// exist, an external, 3-D or table reference, or a reversed range.
10110    /// Unqualified cell and range references, names, literals and calls
10111    /// always plan.
10112    fn formula_may_fail_planning(&self, ast: &formualizer_parse::parser::ASTNode) -> bool {
10113        use formualizer_parse::parser::{ASTNodeType, ReferenceType};
10114        let sheet_missing = |sheet: &Option<String>| {
10115            sheet
10116                .as_deref()
10117                .is_some_and(|s| self.graph.sheet_id(s).is_none())
10118        };
10119        match &ast.node_type {
10120            ASTNodeType::Literal(_) | ASTNodeType::Omitted => false,
10121            ASTNodeType::Reference { reference, .. } => match reference {
10122                ReferenceType::Cell { sheet, .. } => sheet_missing(sheet),
10123                ReferenceType::Range {
10124                    sheet,
10125                    start_row,
10126                    start_col,
10127                    end_row,
10128                    end_col,
10129                    ..
10130                } => {
10131                    sheet_missing(sheet)
10132                        || matches!((start_row, end_row), (Some(a), Some(b)) if a > b)
10133                        || matches!((start_col, end_col), (Some(a), Some(b)) if a > b)
10134                }
10135                ReferenceType::NamedRange(_) => false,
10136                _ => true,
10137            },
10138            ASTNodeType::UnaryOp { expr, .. } => self.formula_may_fail_planning(expr),
10139            ASTNodeType::BinaryOp { left, right, .. } => {
10140                self.formula_may_fail_planning(left) || self.formula_may_fail_planning(right)
10141            }
10142            ASTNodeType::Function { args, .. } => {
10143                args.iter().any(|a| self.formula_may_fail_planning(a))
10144            }
10145            ASTNodeType::Call { callee, args } => {
10146                self.formula_may_fail_planning(callee)
10147                    || args.iter().any(|a| self.formula_may_fail_planning(a))
10148            }
10149            ASTNodeType::Array(rows) => rows
10150                .iter()
10151                .flatten()
10152                .any(|a| self.formula_may_fail_planning(a)),
10153        }
10154    }
10155
10156    /// Begin bulk Arrow ingest for base values (Phase A)
10157    pub fn begin_bulk_ingest_arrow(
10158        &mut self,
10159    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
10160        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
10161    }
10162
10163    /// Begin bulk updates to Arrow store (Phase C)
10164    pub fn begin_bulk_update_arrow(
10165        &mut self,
10166    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
10167        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
10168    }
10169
10170    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
10171        self.graph.sheet_id(sheet).ok_or(
10172            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10173                name: sheet.to_string(),
10174                reason: "Unknown sheet".to_string(),
10175            },
10176        )
10177    }
10178
10179    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
10180        if row_1based == 0 {
10181            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10182        }
10183        Ok(row_1based - 1)
10184    }
10185
10186    fn normalize_row_range_1based(
10187        start_row_1based: u32,
10188        end_row_1based: u32,
10189    ) -> Result<(u32, u32), crate::engine::EditorError> {
10190        if start_row_1based == 0 || end_row_1based == 0 {
10191            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10192        }
10193        if start_row_1based > end_row_1based {
10194            return Err(crate::engine::EditorError::TransactionFailed {
10195                reason: "Row range start is greater than end".to_string(),
10196            });
10197        }
10198        Ok((start_row_1based - 1, end_row_1based - 1))
10199    }
10200
10201    fn invalidate_row_visibility_mask_cache(&self) {
10202        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10203            cache.clear();
10204        }
10205    }
10206
10207    fn set_row_hidden_by_sheet_id(
10208        &mut self,
10209        sheet_id: SheetId,
10210        row0: u32,
10211        hidden: bool,
10212        source: RowVisibilitySource,
10213    ) -> bool {
10214        let changed = {
10215            let state = self.row_visibility.entry(sheet_id).or_default();
10216            state.set_row_hidden(row0, hidden, source)
10217        };
10218
10219        let remove_entry = self
10220            .row_visibility
10221            .get(&sheet_id)
10222            .map(|state| state.is_empty())
10223            .unwrap_or(false);
10224        if remove_entry {
10225            self.row_visibility.remove(&sheet_id);
10226        }
10227
10228        if changed {
10229            self.invalidate_row_visibility_mask_cache();
10230        }
10231
10232        changed
10233    }
10234
10235    fn set_rows_hidden_by_sheet_id(
10236        &mut self,
10237        sheet_id: SheetId,
10238        start_row0: u32,
10239        end_row0: u32,
10240        hidden: bool,
10241        source: RowVisibilitySource,
10242    ) -> bool {
10243        let changed = {
10244            let state = self.row_visibility.entry(sheet_id).or_default();
10245            state.set_rows_hidden(start_row0, end_row0, hidden, source)
10246        };
10247
10248        let remove_entry = self
10249            .row_visibility
10250            .get(&sheet_id)
10251            .map(|state| state.is_empty())
10252            .unwrap_or(false);
10253        if remove_entry {
10254            self.row_visibility.remove(&sheet_id);
10255        }
10256
10257        if changed {
10258            self.invalidate_row_visibility_mask_cache();
10259        }
10260
10261        changed
10262    }
10263
10264    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
10265        if count == 0 {
10266            return;
10267        }
10268        let mut changed = false;
10269        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10270            changed = state.insert_rows(before0, count);
10271            state.is_empty()
10272        } else {
10273            false
10274        };
10275        if remove_entry {
10276            self.row_visibility.remove(&sheet_id);
10277        }
10278        if changed {
10279            self.invalidate_row_visibility_mask_cache();
10280        }
10281    }
10282
10283    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
10284        if count == 0 {
10285            return;
10286        }
10287        let mut changed = false;
10288        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10289            changed = state.delete_rows(start0, count);
10290            state.is_empty()
10291        } else {
10292            false
10293        };
10294        if remove_entry {
10295            self.row_visibility.remove(&sheet_id);
10296        }
10297        if changed {
10298            self.invalidate_row_visibility_mask_cache();
10299        }
10300    }
10301
10302    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10303        if let crate::engine::ChangeEvent::SetRowVisibility {
10304            sheet_id,
10305            row0,
10306            source,
10307            old_hidden,
10308            ..
10309        } = event
10310        {
10311            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
10312        }
10313    }
10314
10315    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10316        if let crate::engine::ChangeEvent::SetRowVisibility {
10317            sheet_id,
10318            row0,
10319            source,
10320            new_hidden,
10321            ..
10322        } = event
10323        {
10324            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
10325        }
10326    }
10327
10328    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10329        for event in events.iter().rev() {
10330            self.apply_inverse_row_visibility_event(event);
10331        }
10332    }
10333
10334    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10335        for event in events {
10336            self.apply_forward_row_visibility_event(event);
10337        }
10338    }
10339
10340    fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10341        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10342            sheet,
10343            row,
10344            col,
10345            old,
10346            ..
10347        } = event
10348        {
10349            self.apply_staged_formula_cell(sheet, *row, *col, old.as_deref());
10350        }
10351    }
10352
10353    fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10354        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10355            sheet,
10356            row,
10357            col,
10358            new,
10359            ..
10360        } = event
10361        {
10362            self.apply_staged_formula_cell(sheet, *row, *col, new.as_deref());
10363        }
10364    }
10365
10366    /// Set a single cell's staged formula text to `target` (clearing it when
10367    /// `None`). Used by undo/redo replay of per-cell staged-formula deltas.
10368    fn apply_staged_formula_cell(&mut self, sheet: &str, row: u32, col: u32, target: Option<&str>) {
10369        match target {
10370            Some(text) => self.stage_formula_text(sheet, row, col, text.to_string()),
10371            None => {
10372                self.clear_staged_formula_text(sheet, row, col);
10373            }
10374        }
10375    }
10376
10377    pub fn set_row_hidden(
10378        &mut self,
10379        sheet: &str,
10380        row_1based: u32,
10381        hidden: bool,
10382        source: RowVisibilitySource,
10383    ) -> Result<(), crate::engine::EditorError> {
10384        self.observe_function_semantic_epoch()
10385            .map_err(crate::engine::EditorError::Excel)?;
10386        self.observe_function_semantic_epoch()
10387            .map_err(crate::engine::EditorError::Excel)?;
10388        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10389        let row0 = Self::normalize_row_1based(row_1based)?;
10390        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
10391            self.record_structural_change(StructuralScope::Region(Region::whole_row(
10392                sheet_id, row0,
10393            )));
10394            self.mark_data_edited();
10395        }
10396        Ok(())
10397    }
10398
10399    pub fn set_rows_hidden(
10400        &mut self,
10401        sheet: &str,
10402        start_row_1based: u32,
10403        end_row_1based: u32,
10404        hidden: bool,
10405        source: RowVisibilitySource,
10406    ) -> Result<(), crate::engine::EditorError> {
10407        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10408        let (start_row0, end_row0) =
10409            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
10410        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
10411            if start_row0 == end_row0 {
10412                self.record_structural_change(StructuralScope::Region(Region::whole_row(
10413                    sheet_id, start_row0,
10414                )));
10415            } else {
10416                self.record_structural_change(StructuralScope::Sheet(sheet_id));
10417            }
10418            self.mark_data_edited();
10419        }
10420        Ok(())
10421    }
10422
10423    pub fn is_row_hidden(
10424        &self,
10425        sheet: &str,
10426        row_1based: u32,
10427        source: Option<RowVisibilitySource>,
10428    ) -> Option<bool> {
10429        let sheet_id = self.graph.sheet_id(sheet)?;
10430        let row0 = row_1based.checked_sub(1)?;
10431        Some(
10432            self.row_visibility
10433                .get(&sheet_id)
10434                .map(|state| state.is_row_hidden(row0, source))
10435                .unwrap_or(false),
10436        )
10437    }
10438
10439    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
10440        let sheet_id = self.graph.sheet_id(sheet)?;
10441        Some(
10442            self.row_visibility
10443                .get(&sheet_id)
10444                .map(|state| state.version())
10445                .unwrap_or(0),
10446        )
10447    }
10448
10449    fn build_row_visibility_mask_for_view(
10450        &self,
10451        view: &RangeView<'_>,
10452        mode: VisibilityMaskMode,
10453    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
10454        let sheet_rows = view.sheet().nrows as usize;
10455        if sheet_rows == 0 || view.start_row() >= sheet_rows {
10456            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
10457        }
10458
10459        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
10460        let start_row0 = view.start_row() as u32;
10461        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
10462        let version = self
10463            .row_visibility
10464            .get(&sheet_id)
10465            .map(|state| state.version())
10466            .unwrap_or(0);
10467        let key = VisibilityMaskCacheKey {
10468            sheet_id,
10469            start_row0,
10470            end_row0,
10471            mode,
10472            version,
10473        };
10474
10475        if let Ok(cache) = self.row_visibility_mask_cache.read()
10476            && let Some(mask) = cache.get(&key)
10477        {
10478            #[cfg(test)]
10479            visibility_mask_test_hooks::inc_hit();
10480            return Some(mask.clone());
10481        }
10482
10483        #[cfg(test)]
10484        visibility_mask_test_hooks::inc_miss();
10485
10486        let state = self.row_visibility.get(&sheet_id);
10487        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
10488        for row0 in start_row0..=end_row0 {
10489            let manual_hidden = state
10490                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
10491                .unwrap_or(false);
10492            let filter_hidden = state
10493                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
10494                .unwrap_or(false);
10495
10496            let include = match mode {
10497                VisibilityMaskMode::IncludeAll => true,
10498                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
10499                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
10500                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
10501                    !(manual_hidden || filter_hidden)
10502                }
10503            };
10504            out.push(include);
10505        }
10506
10507        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
10508        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10509            const MAX_CACHE_ENTRIES: usize = 4096;
10510            if cache.len() >= MAX_CACHE_ENTRIES {
10511                cache.clear();
10512                #[cfg(test)]
10513                visibility_mask_test_hooks::inc_eviction();
10514            }
10515            cache.insert(key, mask.clone());
10516        }
10517
10518        Some(mask)
10519    }
10520
10521    fn observe_function_semantic_epoch(&mut self) -> Result<bool, ExcelError> {
10522        let changes =
10523            crate::function_registry::semantic_changes_since(self.function_semantic_epoch_seen);
10524        let global_changed = changes.epoch != self.function_semantic_epoch_seen;
10525        let provider_revision = self.resolver.planning_semantic_revision();
10526        let provider_changed = provider_revision != self.function_provider_revision_seen;
10527        if !global_changed && !provider_changed {
10528            return Ok(false);
10529        }
10530
10531        let changed = !changes.keys.is_empty();
10532        if global_changed && changed || provider_changed {
10533            self.cached_static_schedule = None;
10534            self.recent_schedules.clear();
10535            self.base_schedule = None;
10536        }
10537        self.function_semantic_epoch_seen = changes.epoch;
10538        self.function_provider_revision_seen = provider_revision;
10539        Ok(false)
10540    }
10541
10542    pub(crate) fn ast_uses_changed_function(
10543        ast: &ASTNode,
10544        changed: &BTreeSet<(String, String)>,
10545    ) -> bool {
10546        match &ast.node_type {
10547            ASTNodeType::Function { name, args } => {
10548                let normalized = name.to_uppercase();
10549                let mut spellings = vec![(String::new(), normalized.clone())];
10550                let mut stripped = normalized.as_str();
10551                loop {
10552                    let Some(rest) = ["_XLFN.", "_XLL.", "_XLWS."]
10553                        .iter()
10554                        .find_map(|prefix| stripped.strip_prefix(prefix))
10555                    else {
10556                        break;
10557                    };
10558                    stripped = rest;
10559                    spellings.push((String::new(), stripped.to_string()));
10560                }
10561                let resolved = crate::function_registry::resolve("", name);
10562                let directly_changed = spellings.iter().any(|spelling| changed.contains(spelling))
10563                    || resolved.as_ref().is_some_and(|resolved| {
10564                        changed.contains(&(
10565                            resolved.namespace.clone(),
10566                            resolved.canonical_name.clone(),
10567                        ))
10568                    });
10569                directly_changed
10570                    || resolved.is_none()
10571                    || args
10572                        .iter()
10573                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10574            }
10575            ASTNodeType::Call { callee, args } => {
10576                Self::ast_uses_changed_function(callee, changed)
10577                    || args
10578                        .iter()
10579                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10580            }
10581            ASTNodeType::UnaryOp { expr, .. } => Self::ast_uses_changed_function(expr, changed),
10582            ASTNodeType::BinaryOp { left, right, .. } => {
10583                Self::ast_uses_changed_function(left, changed)
10584                    || Self::ast_uses_changed_function(right, changed)
10585            }
10586            ASTNodeType::Array(rows) => rows
10587                .iter()
10588                .flatten()
10589                .any(|node| Self::ast_uses_changed_function(node, changed)),
10590            _ => false,
10591        }
10592    }
10593
10594    fn ast_contains_function(ast: &ASTNode) -> bool {
10595        match &ast.node_type {
10596            ASTNodeType::Function { .. } => true,
10597            ASTNodeType::Call { callee, args } => {
10598                Self::ast_contains_function(callee) || args.iter().any(Self::ast_contains_function)
10599            }
10600            ASTNodeType::UnaryOp { expr, .. } => Self::ast_contains_function(expr),
10601            ASTNodeType::BinaryOp { left, right, .. } => {
10602                Self::ast_contains_function(left) || Self::ast_contains_function(right)
10603            }
10604            ASTNodeType::Array(rows) => rows.iter().flatten().any(Self::ast_contains_function),
10605            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => false,
10606        }
10607    }
10608
10609    fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
10610        Region::rows_from(sheet_id, start_row0)
10611    }
10612
10613    fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
10614        Region::cols_from(sheet_id, start_col0)
10615    }
10616
10617    #[cfg(test)]
10618    pub(crate) fn force_non_cycle_schedule_fallback_for_test(&mut self) {
10619        self.force_non_cycle_schedule_fallback_for_test = true;
10620    }
10621
10622    fn materialize_deferred_sheet_before_structural_edit(
10623        &mut self,
10624        sheet: &str,
10625    ) -> Result<(), crate::engine::EditorError> {
10626        if self.staged_formulas.contains_key(sheet) {
10627            self.build_graph_for_sheets([sheet])?;
10628        }
10629        Ok(())
10630    }
10631
10632    fn structural_row_occupancy(
10633        &self,
10634        sheet: &str,
10635        sheet_id: SheetId,
10636    ) -> crate::engine::graph::StructuralOccupancy {
10637        if !self.graph.has_compressed_range_readers() {
10638            return crate::engine::graph::StructuralOccupancy::default();
10639        }
10640        let mut occupancy = self.graph.structural_occupancy(sheet_id);
10641        if let Some(arrow_sheet) = self.arrow_sheets.sheet(sheet) {
10642            occupancy.include_arrow_sheet(arrow_sheet);
10643            occupancy
10644        } else {
10645            // Missing Arrow state cannot prove an apparently empty column empty.
10646            crate::engine::graph::StructuralOccupancy::conservative()
10647        }
10648    }
10649
10650    fn structural_column_occupancy(&self) -> crate::engine::graph::StructuralOccupancy {
10651        // Arrow exposes occupied columns through chunk metadata and overlay maps,
10652        // but has no cheap occupied-row index. Column edits therefore deliberately
10653        // retain conservative cross-axis invalidation instead of scanning cells.
10654        crate::engine::graph::StructuralOccupancy::conservative()
10655    }
10656
10657    /// Insert rows (1-based) and mirror into Arrow store when enabled
10658    pub fn insert_rows(
10659        &mut self,
10660        sheet: &str,
10661        before: u32,
10662        count: u32,
10663    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10664    {
10665        if count == 0 {
10666            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10667        }
10668        self.observe_function_semantic_epoch()
10669            .map_err(crate::engine::EditorError::Excel)?;
10670        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10671        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10672        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10673        let before0 = before.saturating_sub(1);
10674        let affected_region = Self::structural_row_region(sheet_id, before0);
10675        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10676        let summary = {
10677            let mut editor =
10678                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10679            editor.insert_rows(sheet_id, before0, count)?
10680        };
10681        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10682            let before0 = before0 as usize;
10683            asheet.insert_rows(before0, count as usize);
10684        }
10685        self.purge_derived_formats_after_row(sheet_id, before0);
10686        self.mark_moved_formula_vertices_dirty(&summary);
10687        self.clear_computed_overlay_after_row(sheet, before0 as usize);
10688        self.shift_row_visibility_insert(sheet_id, before0, count);
10689        self.record_structural_change(StructuralScope::Region(affected_region));
10690        self.mark_topology_edited();
10691        Ok(summary)
10692    }
10693
10694    /// Delete rows (1-based) and mirror into Arrow store when enabled
10695    pub fn delete_rows(
10696        &mut self,
10697        sheet: &str,
10698        start: u32,
10699        count: u32,
10700    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10701    {
10702        if count == 0 {
10703            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10704        }
10705        self.observe_function_semantic_epoch()
10706            .map_err(crate::engine::EditorError::Excel)?;
10707        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10708        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10709        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10710        let start0 = start.saturating_sub(1);
10711        let affected_region = Self::structural_row_region(sheet_id, start0);
10712        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10713        let summary = {
10714            let mut editor =
10715                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10716            editor.delete_rows(sheet_id, start0, count)?
10717        };
10718        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10719            let start0 = start0 as usize;
10720            asheet.delete_rows(start0, count as usize);
10721        }
10722        self.purge_derived_formats_after_row(sheet_id, start0);
10723        self.mark_moved_formula_vertices_dirty(&summary);
10724        self.clear_computed_overlay_after_row(sheet, start0 as usize);
10725        self.shift_row_visibility_delete(sheet_id, start0, count);
10726        self.record_structural_change(StructuralScope::Region(affected_region));
10727        self.mark_topology_edited();
10728        Ok(summary)
10729    }
10730
10731    /// Insert columns (1-based) and mirror into Arrow store when enabled
10732    pub fn insert_columns(
10733        &mut self,
10734        sheet: &str,
10735        before: u32,
10736        count: u32,
10737    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10738    {
10739        if count == 0 {
10740            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10741        }
10742        self.observe_function_semantic_epoch()
10743            .map_err(crate::engine::EditorError::Excel)?;
10744        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10745        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10746        let sheet_id = self.graph.sheet_id(sheet).ok_or(
10747            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10748                name: sheet.to_string(),
10749                reason: "Unknown sheet".to_string(),
10750            },
10751        )?;
10752        let before0 = before.saturating_sub(1);
10753        let affected_region = Self::structural_col_region(sheet_id, before0);
10754        let occupancy = self.structural_column_occupancy();
10755        let summary = {
10756            let mut editor =
10757                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10758            editor.insert_columns(sheet_id, before0, count)?
10759        };
10760        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10761            let before0 = before0 as usize;
10762            asheet.insert_columns(before0, count as usize);
10763        }
10764        self.purge_derived_formats_after_col(sheet_id, before0);
10765        self.mark_moved_formula_vertices_dirty(&summary);
10766        self.clear_computed_overlay_after_col(sheet, before0 as usize);
10767        self.record_structural_change(StructuralScope::Region(affected_region));
10768        self.mark_topology_edited();
10769        Ok(summary)
10770    }
10771
10772    /// Delete columns (1-based) and mirror into Arrow store when enabled
10773    pub fn delete_columns(
10774        &mut self,
10775        sheet: &str,
10776        start: u32,
10777        count: u32,
10778    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10779    {
10780        if count == 0 {
10781            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10782        }
10783        self.observe_function_semantic_epoch()
10784            .map_err(crate::engine::EditorError::Excel)?;
10785        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10786        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10787        let sheet_id = self.graph.sheet_id(sheet).ok_or(
10788            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10789                name: sheet.to_string(),
10790                reason: "Unknown sheet".to_string(),
10791            },
10792        )?;
10793        let start0 = start.saturating_sub(1);
10794        let affected_region = Self::structural_col_region(sheet_id, start0);
10795        let occupancy = self.structural_column_occupancy();
10796        let summary = {
10797            let mut editor =
10798                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10799            editor.delete_columns(sheet_id, start0, count)?
10800        };
10801        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10802            let start0 = start0 as usize;
10803            asheet.delete_columns(start0, count as usize);
10804        }
10805        self.purge_derived_formats_after_col(sheet_id, start0);
10806        self.mark_moved_formula_vertices_dirty(&summary);
10807        self.clear_computed_overlay_after_col(sheet, start0 as usize);
10808        self.record_structural_change(StructuralScope::Region(affected_region));
10809        self.mark_topology_edited();
10810        Ok(summary)
10811    }
10812    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
10813    fn arrow_used_row_bounds(
10814        &self,
10815        sheet: &str,
10816        start_col: u32,
10817        end_col: u32,
10818    ) -> Option<(u32, u32)> {
10819        let a = self.sheet_store().sheet(sheet)?;
10820        if a.columns.is_empty() {
10821            return None;
10822        }
10823        let sc0 = start_col.saturating_sub(1) as usize;
10824        let ec0 = end_col.saturating_sub(1) as usize;
10825        let col_hi = a.columns.len().saturating_sub(1);
10826        if sc0 > col_hi {
10827            return None;
10828        }
10829        let ec0 = ec0.min(col_hi);
10830        // Pass-scoped cache with snapshot guard
10831        let snap = self.data_snapshot_id();
10832        let mut min_r0: Option<usize> = None;
10833        for ci in sc0..=ec0 {
10834            let sheet_id = self.graph.sheet_id(sheet)?;
10835            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
10836                g.as_ref()
10837                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
10838            }) {
10839                let mv = mv as usize;
10840                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
10841                continue;
10842            }
10843            // Compute and store
10844            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
10845            if let Ok(mut g) = self.row_bounds_cache.write() {
10846                g.get_or_insert_with(|| RowBoundsCache::new(snap))
10847                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
10848            }
10849            if let Some(m) = min_c {
10850                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
10851            }
10852        }
10853        min_r0?;
10854        let mut max_r0: Option<usize> = None;
10855        for ci in sc0..=ec0 {
10856            let sheet_id = self.graph.sheet_id(sheet)?;
10857            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
10858                g.as_ref()
10859                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
10860            }) {
10861                let mv = mv as usize;
10862                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
10863                continue;
10864            }
10865            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
10866            if let Ok(mut g) = self.row_bounds_cache.write() {
10867                g.get_or_insert_with(|| RowBoundsCache::new(snap))
10868                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
10869            }
10870            if let Some(m) = max_c {
10871                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
10872            }
10873        }
10874        match (min_r0, max_r0) {
10875            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
10876            _ => None,
10877        }
10878    }
10879
10880    fn scan_column_used_bounds(
10881        a: &crate::arrow_store::ArrowSheet,
10882        ci: usize,
10883    ) -> (Option<u32>, Option<u32>) {
10884        let col = &a.columns[ci];
10885
10886        // Min: scan dense chunks first, then sparse chunks in ascending index order.
10887        let mut min_r0: Option<u32> = None;
10888        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
10889            let tags = chunk.type_tag.values();
10890            for (off, &t) in tags.iter().enumerate() {
10891                let overlay_non_empty = chunk
10892                    .overlay
10893                    .get(off)
10894                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10895                    .unwrap_or(false)
10896                    || chunk
10897                        .computed_overlay
10898                        .get(off)
10899                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10900                        .unwrap_or(false);
10901                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10902                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10903                        break;
10904                    };
10905                    let row0 = chunk_start + off;
10906                    min_r0 = Some(row0 as u32);
10907                    break;
10908                }
10909            }
10910            if min_r0.is_some() {
10911                break;
10912            }
10913        }
10914        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
10915            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
10916            sparse_idxs.sort_unstable();
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 (off, &t) in tags.iter().enumerate() {
10926                    let overlay_non_empty = chunk
10927                        .overlay
10928                        .get(off)
10929                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10930                        .unwrap_or(false)
10931                        || chunk
10932                            .computed_overlay
10933                            .get(off)
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 + off;
10938                        min_r0 = Some(row0 as u32);
10939                        break;
10940                    }
10941                }
10942                if min_r0.is_some() {
10943                    break;
10944                }
10945            }
10946        }
10947
10948        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
10949        let mut max_r0: Option<u32> = None;
10950        if !col.sparse_chunks.is_empty() {
10951            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
10952            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
10953            for chunk_idx in sparse_idxs {
10954                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
10955                    continue;
10956                };
10957                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10958                    continue;
10959                };
10960                let tags = chunk.type_tag.values();
10961                for (rev_idx, &t) in tags.iter().enumerate().rev() {
10962                    let overlay_non_empty = chunk
10963                        .overlay
10964                        .get(rev_idx)
10965                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10966                        .unwrap_or(false)
10967                        || chunk
10968                            .computed_overlay
10969                            .get(rev_idx)
10970                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10971                            .unwrap_or(false);
10972                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10973                        let row0 = chunk_start + rev_idx;
10974                        max_r0 = Some(row0 as u32);
10975                        break;
10976                    }
10977                }
10978                if max_r0.is_some() {
10979                    break;
10980                }
10981            }
10982        }
10983        if max_r0.is_none() {
10984            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
10985                let tags = chunk.type_tag.values();
10986                for (rev_idx, &t) in tags.iter().enumerate().rev() {
10987                    let overlay_non_empty = chunk
10988                        .overlay
10989                        .get(rev_idx)
10990                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10991                        .unwrap_or(false)
10992                        || chunk
10993                            .computed_overlay
10994                            .get(rev_idx)
10995                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
10996                            .unwrap_or(false);
10997                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
10998                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
10999                            break;
11000                        };
11001                        let row0 = chunk_start + rev_idx;
11002                        max_r0 = Some(row0 as u32);
11003                        break;
11004                    }
11005                }
11006                if max_r0.is_some() {
11007                    break;
11008                }
11009            }
11010        }
11011
11012        (min_r0, max_r0)
11013    }
11014
11015    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
11016    fn arrow_used_col_bounds(
11017        &self,
11018        sheet: &str,
11019        start_row: u32,
11020        end_row: u32,
11021    ) -> Option<(u32, u32)> {
11022        let a = self.sheet_store().sheet(sheet)?;
11023        if a.columns.is_empty() {
11024            return None;
11025        }
11026        let sr0 = start_row.saturating_sub(1) as usize;
11027        let er0 = end_row.saturating_sub(1) as usize;
11028        if sr0 > er0 {
11029            return None;
11030        }
11031        // Map start/end rows into chunk ranges
11032        // We will scan each column for any non-empty within [sr0..=er0]
11033        let mut min_c0: Option<usize> = None;
11034        let mut max_c0: Option<usize> = None;
11035        // Precompute chunk bounds for row range
11036        for (ci, col) in a.columns.iter().enumerate() {
11037            let mut any_in_range = false;
11038
11039            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
11040                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11041                    return false;
11042                };
11043                let chunk_len = chunk.type_tag.len();
11044                if chunk_len == 0 {
11045                    return false;
11046                }
11047                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
11048                // check intersection
11049                if sr0 > chunk_end || er0 < chunk_start {
11050                    return false;
11051                }
11052                let start_off = sr0.max(chunk_start) - chunk_start;
11053                let end_off = er0.min(chunk_end) - chunk_start;
11054                let tags = chunk.type_tag.values();
11055                for off in start_off..=end_off {
11056                    let overlay_non_empty = chunk
11057                        .overlay
11058                        .get(off)
11059                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11060                        .unwrap_or(false)
11061                        || chunk
11062                            .computed_overlay
11063                            .get(off)
11064                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11065                            .unwrap_or(false);
11066                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
11067                        return true;
11068                    }
11069                }
11070                false
11071            };
11072
11073            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
11074                if scan_chunk(chunk_idx, chunk) {
11075                    any_in_range = true;
11076                    break;
11077                }
11078            }
11079
11080            if !any_in_range && !col.sparse_chunks.is_empty() {
11081                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
11082                    if scan_chunk(chunk_idx, chunk) {
11083                        any_in_range = true;
11084                        break;
11085                    }
11086                }
11087            }
11088
11089            if any_in_range {
11090                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
11091                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
11092            }
11093        }
11094        match (min_c0, max_c0) {
11095            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
11096            _ => None,
11097        }
11098    }
11099
11100    fn formula_row_bounds_for_columns(
11101        &self,
11102        sheet: &str,
11103        start_col: u32,
11104        end_col: u32,
11105    ) -> Option<(u32, u32)> {
11106        let sheet_id = self.graph.sheet_id(sheet)?;
11107        let sc0 = start_col.saturating_sub(1);
11108        let ec0 = end_col.saturating_sub(1);
11109        let mut min_r0: Option<u32> = None;
11110        let mut max_r0: Option<u32> = None;
11111
11112        if self.graph.sheet_index(sheet_id).is_some() {
11113            for vid in self.graph.vertices_in_cols(sheet_id, sc0, ec0) {
11114                if !matches!(
11115                    self.graph.get_vertex_kind(vid),
11116                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11117                ) {
11118                    continue;
11119                }
11120                let Some(row0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.row()) else {
11121                    continue;
11122                };
11123                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11124                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11125            }
11126        } else {
11127            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11128                if !matches!(
11129                    self.graph.get_vertex_kind(vid),
11130                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11131                ) {
11132                    continue;
11133                }
11134                let col0 = coord.col();
11135                if col0 < sc0 || col0 > ec0 {
11136                    continue;
11137                }
11138                let row0 = coord.row();
11139                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11140                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11141            }
11142        }
11143
11144        match (min_r0, max_r0) {
11145            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11146            _ => None,
11147        }
11148    }
11149
11150    fn formula_col_bounds_for_rows(
11151        &self,
11152        sheet: &str,
11153        start_row: u32,
11154        end_row: u32,
11155    ) -> Option<(u32, u32)> {
11156        let sheet_id = self.graph.sheet_id(sheet)?;
11157        let sr0 = start_row.saturating_sub(1);
11158        let er0 = end_row.saturating_sub(1);
11159        let mut min_c0: Option<u32> = None;
11160        let mut max_c0: Option<u32> = None;
11161
11162        if self.graph.sheet_index(sheet_id).is_some() {
11163            for vid in self.graph.vertices_in_rows(sheet_id, sr0, er0) {
11164                if !matches!(
11165                    self.graph.get_vertex_kind(vid),
11166                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11167                ) {
11168                    continue;
11169                }
11170                let Some(col0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.col()) else {
11171                    continue;
11172                };
11173                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11174                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11175            }
11176        } else {
11177            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11178                if !matches!(
11179                    self.graph.get_vertex_kind(vid),
11180                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11181                ) {
11182                    continue;
11183                }
11184                let row0 = coord.row();
11185                if row0 < sr0 || row0 > er0 {
11186                    continue;
11187                }
11188                let col0 = coord.col();
11189                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11190                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11191            }
11192        }
11193
11194        match (min_c0, max_c0) {
11195            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11196            _ => None,
11197        }
11198    }
11199
11200    fn union_used_bounds(
11201        first: Option<(u32, u32)>,
11202        second: Option<(u32, u32)>,
11203    ) -> Option<(u32, u32)> {
11204        match (first, second) {
11205            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
11206            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
11207            (None, None) => None,
11208        }
11209    }
11210
11211    /// Mirror a single cell value into the Arrow overlay if enabled.
11212    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
11213    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
11214        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11215            return;
11216        }
11217        if self.arrow_sheets.sheet(sheet).is_none() {
11218            self.arrow_sheets
11219                .sheets
11220                .push(crate::arrow_store::ArrowSheet {
11221                    name: std::sync::Arc::<str>::from(sheet),
11222                    date_system: self.config.date_system,
11223                    columns: Vec::new(),
11224                    nrows: 0,
11225                    chunk_starts: Vec::new(),
11226                    chunk_rows: 32 * 1024,
11227                });
11228        }
11229
11230        let row0 = row.saturating_sub(1) as usize;
11231        let col0 = col.saturating_sub(1) as usize;
11232
11233        let asheet = self
11234            .arrow_sheets
11235            .sheet_mut(sheet)
11236            .expect("ArrowSheet must exist");
11237
11238        let cur_cols = asheet.columns.len();
11239        if col0 >= cur_cols {
11240            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11241        }
11242
11243        if row0 >= asheet.nrows as usize {
11244            if asheet.columns.is_empty() {
11245                asheet.insert_columns(0, 1);
11246            }
11247            asheet.ensure_row_capacity(row0 + 1);
11248        }
11249        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
11250            let ov =
11251                crate::arrow_store::OverlayValue::from_literal_value(value, asheet.date_system);
11252            let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
11253                let _ = ch.overlay.set(in_off, ov);
11254                let format = match value {
11255                    LiteralValue::Date(_) => Some(crate::format::FormatId::DATE),
11256                    LiteralValue::DateTime(_) => Some(crate::format::FormatId::DATETIME),
11257                    LiteralValue::Time(_) => Some(crate::format::FormatId::TIME),
11258                    LiteralValue::Duration(_) => Some(crate::format::FormatId::DURATION),
11259                    _ => None,
11260                };
11261                ch.overlay.set_format(in_off, format);
11262                // A user edit must invalidate any computed (formula/spill) overlay entry at
11263                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
11264                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
11265                // the edited base value under the read cascade.
11266                ch.computed_overlay.remove(in_off)
11267            } else {
11268                return;
11269            };
11270            // Heuristic compaction: > len/50 or > 1024
11271            let abs_threshold = 1024usize;
11272            let frac_den = 50usize;
11273            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
11274            if freed > 0 {
11275                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
11276            }
11277            self.adjust_computed_overlay_bytes(computed_delta);
11278        }
11279    }
11280
11281    /// Remove a delta-overlay entry for a single cell (if present).
11282    ///
11283    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
11284    /// does not continue to mask computed overlay outputs.
11285    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
11286        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11287            return;
11288        }
11289        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11290            return;
11291        };
11292        let row0 = row.saturating_sub(1) as usize;
11293        let col0 = col.saturating_sub(1) as usize;
11294        if row0 >= asheet.nrows as usize {
11295            return;
11296        }
11297        if col0 >= asheet.columns.len() {
11298            return;
11299        }
11300        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11301            return;
11302        };
11303        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
11304            let _ = ch.overlay.remove(in_off);
11305        }
11306    }
11307
11308    fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
11309        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11310            return;
11311        }
11312
11313        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11314            return;
11315        };
11316        if start_row0 >= asheet.nrows as usize {
11317            return;
11318        }
11319
11320        let starts = asheet.chunk_starts.clone();
11321        let nrows = asheet.nrows as usize;
11322        let mut delta = 0isize;
11323        for col in &mut asheet.columns {
11324            for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
11325                let Some(&chunk_start) = starts.get(chunk_idx) else {
11326                    continue;
11327                };
11328                let chunk_end = starts
11329                    .get(chunk_idx + 1)
11330                    .copied()
11331                    .unwrap_or(nrows)
11332                    .min(chunk_start.saturating_add(ch.len()));
11333                if chunk_end <= start_row0 {
11334                    continue;
11335                }
11336                if chunk_start >= start_row0 {
11337                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11338                } else {
11339                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11340                    delta = delta
11341                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11342                }
11343            }
11344
11345            for (chunk_idx, ch) in &mut col.sparse_chunks {
11346                let Some(&chunk_start) = starts.get(*chunk_idx) else {
11347                    continue;
11348                };
11349                let chunk_end = starts
11350                    .get(*chunk_idx + 1)
11351                    .copied()
11352                    .unwrap_or(nrows)
11353                    .min(chunk_start.saturating_add(ch.len()));
11354                if chunk_end <= start_row0 {
11355                    continue;
11356                }
11357                if chunk_start >= start_row0 {
11358                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11359                } else {
11360                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11361                    delta = delta
11362                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11363                }
11364            }
11365        }
11366        self.adjust_computed_overlay_bytes(delta);
11367    }
11368
11369    fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
11370        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11371            return;
11372        }
11373
11374        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11375            return;
11376        };
11377        if start_col0 >= asheet.columns.len() {
11378            return;
11379        }
11380
11381        let mut delta = 0isize;
11382        for col in asheet.columns.iter_mut().skip(start_col0) {
11383            for ch in &mut col.chunks {
11384                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11385            }
11386            for ch in col.sparse_chunks.values_mut() {
11387                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11388            }
11389        }
11390        self.adjust_computed_overlay_bytes(delta);
11391    }
11392
11393    #[inline]
11394    fn literal_to_overlay_value(
11395        value: &LiteralValue,
11396        date_system: crate::engine::DateSystem,
11397    ) -> crate::arrow_store::OverlayValue {
11398        crate::arrow_store::OverlayValue::from_literal_value(value, date_system)
11399    }
11400
11401    fn arrow_sheet_date_system(&self, sheet: &str) -> crate::engine::DateSystem {
11402        self.arrow_sheets
11403            .sheet(sheet)
11404            .map(|sheet| sheet.date_system)
11405            .unwrap_or(self.config.date_system)
11406    }
11407
11408    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
11409    /// absent and explicit `Empty`.
11410    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11411        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11412            return None;
11413        }
11414        let asheet = self.arrow_sheets.sheet(sheet)?;
11415        let row0 = row.saturating_sub(1) as usize;
11416        let col0 = col.saturating_sub(1) as usize;
11417        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11418            return None;
11419        }
11420        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11421        let ch = asheet.columns[col0].chunk(ch_idx)?;
11422        ch.overlay
11423            .get_scalar(in_off)
11424            .map(|ov| ov.to_literal_for(asheet.date_system))
11425    }
11426
11427    /// Read a single cell's computed overlay entry (if present), preserving the distinction
11428    /// between absent and explicit `Empty`.
11429    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11430        if !(self.config.arrow_storage_enabled
11431            && self.config.delta_overlay_enabled
11432            && self.config.write_formula_overlay_enabled)
11433        {
11434            return None;
11435        }
11436        let asheet = self.arrow_sheets.sheet(sheet)?;
11437        let row0 = row.saturating_sub(1) as usize;
11438        let col0 = col.saturating_sub(1) as usize;
11439        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11440            return None;
11441        }
11442        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11443        let ch = asheet.columns[col0].chunk(ch_idx)?;
11444        ch.computed_overlay
11445            .get_scalar(in_off)
11446            .map(|ov| ov.to_literal_for(asheet.date_system))
11447    }
11448
11449    fn set_delta_overlay_cell_raw(
11450        &mut self,
11451        sheet: &str,
11452        row: u32,
11453        col: u32,
11454        value: Option<LiteralValue>,
11455    ) {
11456        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11457            return;
11458        }
11459
11460        self.ensure_arrow_sheet(sheet);
11461        let date_system = self.arrow_sheet_date_system(sheet);
11462        let ov_opt = value
11463            .as_ref()
11464            .map(|value| Self::literal_to_overlay_value(value, date_system));
11465        let row0 = row.saturating_sub(1) as usize;
11466        let col0 = col.saturating_sub(1) as usize;
11467        let asheet = self
11468            .arrow_sheets
11469            .sheet_mut(sheet)
11470            .expect("ArrowSheet must exist");
11471
11472        let cur_cols = asheet.columns.len();
11473        if col0 >= cur_cols {
11474            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11475        }
11476        if row0 >= asheet.nrows as usize {
11477            if asheet.columns.is_empty() {
11478                asheet.insert_columns(0, 1);
11479            }
11480            asheet.ensure_row_capacity(row0 + 1);
11481        }
11482
11483        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11484            return;
11485        };
11486        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11487            return;
11488        };
11489
11490        if let Some(ov) = ov_opt {
11491            let _ = ch.overlay.set(in_off, ov);
11492        } else {
11493            let _ = ch.overlay.remove(in_off);
11494        }
11495    }
11496
11497    fn set_computed_overlay_cell_raw(
11498        &mut self,
11499        sheet: &str,
11500        row: u32,
11501        col: u32,
11502        value: Option<LiteralValue>,
11503    ) {
11504        if !(self.config.arrow_storage_enabled
11505            && self.config.delta_overlay_enabled
11506            && self.config.write_formula_overlay_enabled)
11507        {
11508            return;
11509        }
11510
11511        self.ensure_arrow_sheet(sheet);
11512        let date_system = self.arrow_sheet_date_system(sheet);
11513        let ov_opt = value
11514            .as_ref()
11515            .map(|value| Self::literal_to_overlay_value(value, date_system));
11516        let row0 = row.saturating_sub(1) as usize;
11517        let col0 = col.saturating_sub(1) as usize;
11518        let asheet = self
11519            .arrow_sheets
11520            .sheet_mut(sheet)
11521            .expect("ArrowSheet must exist");
11522
11523        let cur_cols = asheet.columns.len();
11524        if col0 >= cur_cols {
11525            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11526        }
11527        if row0 >= asheet.nrows as usize {
11528            if asheet.columns.is_empty() {
11529                asheet.insert_columns(0, 1);
11530            }
11531            asheet.ensure_row_capacity(row0 + 1);
11532        }
11533
11534        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11535            return;
11536        };
11537        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11538            return;
11539        };
11540
11541        let delta = if let Some(ov) = ov_opt {
11542            ch.computed_overlay.set(in_off, ov)
11543        } else {
11544            ch.computed_overlay.remove(in_off)
11545        };
11546        self.adjust_computed_overlay_bytes(delta);
11547    }
11548
11549    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
11550        use crate::engine::ArrowOp;
11551
11552        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
11553            Box::new(batch.ops.iter().rev())
11554        } else {
11555            Box::new(batch.ops.iter())
11556        };
11557
11558        for op in iter {
11559            match op {
11560                ArrowOp::SetDeltaCell {
11561                    sheet_id,
11562                    row0,
11563                    col0,
11564                    old,
11565                    new,
11566                } => {
11567                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11568                    let v = if undo { old.clone() } else { new.clone() };
11569                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11570                }
11571                ArrowOp::SetComputedCell {
11572                    sheet_id,
11573                    row0,
11574                    col0,
11575                    old,
11576                    new,
11577                } => {
11578                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11579                    let v = if undo { old.clone() } else { new.clone() };
11580                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11581                }
11582                ArrowOp::RestoreComputedRect {
11583                    sheet_id,
11584                    sr0,
11585                    sc0,
11586                    er0,
11587                    ec0,
11588                    old,
11589                    new,
11590                } => {
11591                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11592                    let vals = if undo { old } else { new };
11593                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
11594                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
11595                    for r in 0..height {
11596                        for c in 0..width {
11597                            let v = vals
11598                                .get(r)
11599                                .and_then(|row| row.get(c))
11600                                .cloned()
11601                                .unwrap_or(LiteralValue::Empty);
11602                            self.set_computed_overlay_cell_raw(
11603                                &sheet,
11604                                *sr0 + 1 + r as u32,
11605                                *sc0 + 1 + c as u32,
11606                                Some(v),
11607                            );
11608                        }
11609                    }
11610                }
11611                ArrowOp::InsertRows {
11612                    sheet_id,
11613                    before0,
11614                    count,
11615                } => {
11616                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11617                    self.ensure_arrow_sheet(&sheet);
11618                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11619                        if undo {
11620                            asheet.delete_rows(*before0 as usize, *count as usize);
11621                        } else {
11622                            asheet.insert_rows(*before0 as usize, *count as usize);
11623                        }
11624                    }
11625                    self.purge_derived_formats_after_row(*sheet_id, *before0);
11626                }
11627                ArrowOp::InsertCols {
11628                    sheet_id,
11629                    before0,
11630                    count,
11631                } => {
11632                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11633                    self.ensure_arrow_sheet(&sheet);
11634                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11635                        if undo {
11636                            asheet.delete_columns(*before0 as usize, *count as usize);
11637                        } else {
11638                            asheet.insert_columns(*before0 as usize, *count as usize);
11639                        }
11640                    }
11641                    self.purge_derived_formats_after_col(*sheet_id, *before0);
11642                }
11643            }
11644        }
11645    }
11646
11647    fn record_spill_ops_into_arrow_undo(
11648        &mut self,
11649        undo: &mut crate::engine::ArrowUndoBatch,
11650        events: &[crate::engine::ChangeEvent],
11651    ) {
11652        use crate::engine::ChangeEvent;
11653        use formualizer_common::LiteralValue;
11654
11655        #[allow(clippy::type_complexity)]
11656        let rect_from_snapshot =
11657            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
11658             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
11659                if snap.target_cells.is_empty() {
11660                    return None;
11661                }
11662                let sheet_id = snap.target_cells[0].sheet_id;
11663                let sr0 = snap.target_cells[0].coord.row();
11664                let sc0 = snap.target_cells[0].coord.col();
11665                if snap.values.is_empty() || snap.values[0].is_empty() {
11666                    return None;
11667                }
11668                let h = snap.values.len() as u32;
11669                let w = snap.values[0].len() as u32;
11670                let er0 = sr0.saturating_add(h.saturating_sub(1));
11671                let ec0 = sc0.saturating_add(w.saturating_sub(1));
11672                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
11673            };
11674
11675        for ev in events {
11676            match ev {
11677                ChangeEvent::SpillCommitted { old, new, .. } => {
11678                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
11679                        let old_vals = if let Some(old_snap) = old {
11680                            rect_from_snapshot(old_snap)
11681                                .map(|(_, _, _, _, _, v)| v)
11682                                .unwrap_or_else(|| {
11683                                    vec![
11684                                        vec![LiteralValue::Empty; new_vals[0].len()];
11685                                        new_vals.len()
11686                                    ]
11687                                })
11688                        } else {
11689                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
11690                        };
11691                        undo.record_restore_computed_rect(
11692                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11693                        );
11694                    }
11695                }
11696                ChangeEvent::SpillCleared { old, .. } => {
11697                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
11698                        let new_vals =
11699                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
11700                        undo.record_restore_computed_rect(
11701                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11702                        );
11703                    }
11704                }
11705                _ => {}
11706            }
11707        }
11708    }
11709
11710    /// Mirror a value into the computed overlay (formula/spill outputs).
11711    ///
11712    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
11713    /// If the cap is exceeded, computed overlays are compacted into base lanes.
11714    fn mirror_value_to_computed_overlay(
11715        &mut self,
11716        sheet: &str,
11717        row: u32,
11718        col: u32,
11719        value: &LiteralValue,
11720    ) {
11721        if !(self.config.arrow_storage_enabled
11722            && self.config.delta_overlay_enabled
11723            && self.config.write_formula_overlay_enabled)
11724        {
11725            return;
11726        }
11727        if self.computed_overlay_mirroring_disabled {
11728            return;
11729        }
11730
11731        let date_system = self.arrow_sheet_date_system(sheet);
11732        let ov = Self::literal_to_overlay_value(value, date_system);
11733        self.write_computed_overlay_value_0based(
11734            sheet,
11735            row.saturating_sub(1),
11736            col.saturating_sub(1),
11737            ov,
11738        );
11739    }
11740
11741    fn record_derived_format(&self, vertex_id: VertexId, format: Option<crate::format::FormatId>) {
11742        if let Some(cell) = self.graph.get_cell_ref(vertex_id) {
11743            self.record_derived_format_at(cell, format);
11744        }
11745    }
11746
11747    fn record_derived_format_at(&self, cell: CellRef, format: Option<crate::format::FormatId>) {
11748        #[cfg(test)]
11749        self.derived_format_operations_for_test
11750            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11751        let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
11752        self.derived_formats.set(cell, format);
11753    }
11754
11755    fn clear_cell_format_state(&mut self, sheet: &str, cell: CellRef) {
11756        self.derived_formats.set(cell, None);
11757        if let Some(arrow) = self.arrow_sheets.sheet_mut(sheet) {
11758            arrow.clear_format(cell.coord.row() as usize, cell.coord.col() as usize);
11759        }
11760    }
11761
11762    fn clear_logged_cell_format_states(&mut self, events: &[ChangeEvent]) {
11763        let cells = events
11764            .iter()
11765            .filter_map(|event| match event {
11766                ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
11767                    Some(*addr)
11768                }
11769                _ => None,
11770            })
11771            .collect::<FxHashSet<_>>();
11772        for cell in cells {
11773            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
11774            self.clear_cell_format_state(&sheet, cell);
11775        }
11776    }
11777
11778    fn purge_derived_formats_after_row(&mut self, sheet_id: SheetId, start0: u32) {
11779        self.derived_formats
11780            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.row() < start0);
11781    }
11782
11783    fn purge_derived_formats_after_col(&mut self, sheet_id: SheetId, start0: u32) {
11784        self.derived_formats
11785            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.col() < start0);
11786    }
11787
11788    fn purge_derived_formats_for_sheet(&mut self, sheet_id: SheetId) {
11789        self.derived_formats
11790            .retain(|cell| cell.sheet_id != sheet_id);
11791    }
11792
11793    #[cfg(test)]
11794    pub(crate) fn debug_computed_overlay_format_0based(
11795        &self,
11796        sheet: &str,
11797        row0: u32,
11798        col0: u32,
11799    ) -> Option<crate::format::FormatId> {
11800        let sheet = self.arrow_sheets.sheet(sheet)?;
11801        let (chunk_idx, row_in_chunk) = sheet.chunk_of_row(row0 as usize)?;
11802        sheet
11803            .columns
11804            .get(col0 as usize)?
11805            .chunk(chunk_idx)?
11806            .computed_overlay
11807            .get_format(row_in_chunk)
11808    }
11809
11810    #[cfg(test)]
11811    pub(crate) fn debug_computed_overlay_chunk_has_formats_0based(
11812        &self,
11813        sheet: &str,
11814        row0: u32,
11815        col0: u32,
11816    ) -> bool {
11817        let Some(sheet) = self.arrow_sheets.sheet(sheet) else {
11818            return false;
11819        };
11820        let Some((chunk_idx, _)) = sheet.chunk_of_row(row0 as usize) else {
11821            return false;
11822        };
11823        sheet
11824            .columns
11825            .get(col0 as usize)
11826            .and_then(|column| column.chunk(chunk_idx))
11827            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
11828    }
11829
11830    #[cfg(test)]
11831    pub(crate) fn debug_clear_derived_format_0based(&mut self, sheet: &str, row0: u32, col0: u32) {
11832        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
11833            self.derived_formats
11834                .set(CellRef::new_absolute(sheet_id, row0, col0), None);
11835        }
11836    }
11837
11838    #[cfg(test)]
11839    pub(crate) fn debug_record_derived_format_0based(
11840        &self,
11841        sheet: &str,
11842        row0: u32,
11843        col0: u32,
11844        format: Option<crate::format::FormatId>,
11845    ) {
11846        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
11847            self.record_derived_format_at(CellRef::new_absolute(sheet_id, row0, col0), format);
11848        }
11849    }
11850
11851    #[cfg(test)]
11852    pub(crate) fn debug_derived_format_0based(
11853        &self,
11854        sheet: &str,
11855        row0: u32,
11856        col0: u32,
11857    ) -> Option<crate::format::FormatId> {
11858        let sheet_id = self.graph.sheet_id(sheet)?;
11859        self.derived_formats
11860            .get(&CellRef::new_absolute(sheet_id, row0, col0))
11861    }
11862
11863    #[cfg(test)]
11864    pub(crate) fn debug_reset_format_write_operation_counts(&mut self) {
11865        self.derived_format_operations_for_test
11866            .store(0, std::sync::atomic::Ordering::Relaxed);
11867        self.computed_overlay_set_explicit_entry_operations_for_test = 0;
11868        self.computed_overlay_stale_clear_range_effects_for_test = 0;
11869        self.computed_overlay_stale_clear_offset_attempts_for_test = 0;
11870        self.computed_format_vector_allocations_for_test
11871            .store(0, std::sync::atomic::Ordering::Relaxed);
11872    }
11873
11874    #[cfg(test)]
11875    pub(crate) fn debug_format_write_operation_counts(&self) -> (u64, u64, u64, u64, u64) {
11876        (
11877            self.derived_format_operations_for_test
11878                .load(std::sync::atomic::Ordering::Relaxed),
11879            self.computed_overlay_set_explicit_entry_operations_for_test,
11880            self.computed_format_vector_allocations_for_test
11881                .load(std::sync::atomic::Ordering::Relaxed),
11882            self.computed_overlay_stale_clear_range_effects_for_test,
11883            self.computed_overlay_stale_clear_offset_attempts_for_test,
11884        )
11885    }
11886
11887    fn write_computed_overlay_format_0based(
11888        &mut self,
11889        sheet: &str,
11890        row0: u32,
11891        col0: u32,
11892        format: Option<crate::format::FormatId>,
11893    ) {
11894        self.ensure_arrow_sheet(sheet);
11895        let (row0, col0) = (row0 as usize, col0 as usize);
11896        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11897            return;
11898        };
11899        if col0 >= asheet.columns.len() {
11900            asheet.insert_columns(asheet.columns.len(), col0 + 1 - asheet.columns.len());
11901        }
11902        if row0 >= asheet.nrows as usize {
11903            asheet.ensure_row_capacity(row0 + 1);
11904        }
11905        let Some((chunk, offset)) = asheet.chunk_of_row(row0) else {
11906            return;
11907        };
11908        if let Some(chunk) = asheet.ensure_column_chunk_mut(col0, chunk) {
11909            chunk.computed_overlay.set_format(offset, format);
11910        }
11911    }
11912
11913    /// One unbuffered computed write of `value` and its derived `format` at
11914    /// `cell` (a single sheet lookup; the same writes as
11915    /// `write_computed_overlay_value_0based` then
11916    /// `write_computed_overlay_format_0based`).
11917    fn write_computed_cell_0based(
11918        &mut self,
11919        cell: CellRef,
11920        value: &LiteralValue,
11921        format: Option<crate::format::FormatId>,
11922    ) {
11923        if !(self.config.arrow_storage_enabled
11924            && self.config.delta_overlay_enabled
11925            && self.config.write_formula_overlay_enabled)
11926            || self.computed_overlay_mirroring_disabled
11927        {
11928            return;
11929        }
11930        let sheet = self.graph.sheet_name(cell.sheet_id);
11931        let index = match self
11932            .arrow_sheets
11933            .sheets
11934            .iter()
11935            .position(|s| s.name.as_ref() == sheet)
11936        {
11937            Some(index) => index,
11938            None => {
11939                let sheet = sheet.to_string();
11940                self.ensure_arrow_sheet(&sheet);
11941                self.arrow_sheets.sheets.len() - 1
11942            }
11943        };
11944        let (row0, col0) = (cell.coord.row() as usize, cell.coord.col() as usize);
11945        let asheet = &mut self.arrow_sheets.sheets[index];
11946        let ov = Self::literal_to_overlay_value(value, asheet.date_system);
11947        let cur_cols = asheet.columns.len();
11948        if col0 >= cur_cols {
11949            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11950        }
11951        if row0 >= asheet.nrows as usize {
11952            asheet.ensure_row_capacity(row0 + 1);
11953        }
11954        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11955            return;
11956        };
11957        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11958            return;
11959        };
11960        let delta = ch.computed_overlay.set_scalar(in_off, ov);
11961        ch.computed_overlay.set_format(in_off, format);
11962        self.adjust_computed_overlay_bytes(delta);
11963        if let Some(cap) = self.config.max_overlay_memory_bytes
11964            && self.computed_overlay_bytes_estimate > cap
11965        {
11966            self.disable_computed_overlay_mirroring_due_to_budget(cap);
11967        }
11968    }
11969
11970    fn write_computed_overlay_value_0based(
11971        &mut self,
11972        sheet: &str,
11973        row0: u32,
11974        col0: u32,
11975        value: OverlayValue,
11976    ) {
11977        if !(self.config.arrow_storage_enabled
11978            && self.config.delta_overlay_enabled
11979            && self.config.write_formula_overlay_enabled)
11980        {
11981            return;
11982        }
11983        if self.computed_overlay_mirroring_disabled {
11984            return;
11985        }
11986
11987        self.ensure_arrow_sheet(sheet);
11988
11989        let row0 = row0 as usize;
11990        let col0 = col0 as usize;
11991        let asheet = self
11992            .arrow_sheets
11993            .sheet_mut(sheet)
11994            .expect("ArrowSheet must exist");
11995
11996        let cur_cols = asheet.columns.len();
11997        if col0 >= cur_cols {
11998            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11999        }
12000
12001        if row0 >= asheet.nrows as usize {
12002            if asheet.columns.is_empty() {
12003                asheet.insert_columns(0, 1);
12004            }
12005            asheet.ensure_row_capacity(row0 + 1);
12006        }
12007
12008        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12009            return;
12010        };
12011        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12012            return;
12013        };
12014
12015        let delta = ch.computed_overlay.set_scalar(in_off, value);
12016        self.adjust_computed_overlay_bytes(delta);
12017
12018        if let Some(cap) = self.config.max_overlay_memory_bytes
12019            && self.computed_overlay_bytes_estimate > cap
12020        {
12021            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12022        }
12023    }
12024
12025    pub(crate) fn plan_computed_write_coalescing(
12026        &self,
12027        buffer: &ComputedWriteBuffer,
12028    ) -> ComputedWriteCoalescingPlan {
12029        self.plan_computed_write_coalescing_from_writes(
12030            buffer.writes().iter().cloned(),
12031            buffer.formats_present,
12032        )
12033    }
12034
12035    fn plan_owned_computed_write_coalescing(
12036        &self,
12037        writes: Vec<ComputedWrite>,
12038        formats_present: bool,
12039    ) -> ComputedWriteCoalescingPlan {
12040        self.plan_computed_write_coalescing_from_writes(writes, formats_present)
12041    }
12042
12043    fn plan_computed_write_coalescing_from_writes(
12044        &self,
12045        writes: impl IntoIterator<Item = ComputedWrite>,
12046        formats_present: bool,
12047    ) -> ComputedWriteCoalescingPlan {
12048        let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
12049            BTreeMap::new();
12050        let mut input_cells = 0usize;
12051        // One sheet lookup per sheet run of writes, not per cell.
12052        let mut located: Option<(SheetId, Option<&crate::arrow_store::ArrowSheet>)> = None;
12053
12054        for write in writes {
12055            match write {
12056                ComputedWrite::Cell {
12057                    seq,
12058                    sheet_id,
12059                    row0,
12060                    col0,
12061                    value,
12062                    format_id,
12063                } => {
12064                    input_cells = input_cells.saturating_add(1);
12065                    let sheet = match located {
12066                        Some((id, sheet)) if id == sheet_id => sheet,
12067                        _ => {
12068                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12069                            located = Some((sheet_id, sheet));
12070                            sheet
12071                        }
12072                    };
12073                    let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12074                        Some(sheet) => {
12075                            Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize)
12076                        }
12077                        None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12078                            row0 as usize,
12079                            32 * 1024,
12080                        ),
12081                    };
12082                    groups
12083                        .entry(ComputedWriteChunkKey {
12084                            sheet_id,
12085                            col0,
12086                            chunk_idx,
12087                            chunk_start_row0,
12088                        })
12089                        .or_default()
12090                        .push(ComputedWriteChunkEntryPlan {
12091                            row_in_chunk,
12092                            seq,
12093                            value,
12094                            format_id,
12095                        });
12096                }
12097                ComputedWrite::Run {
12098                    seq,
12099                    sheet_id,
12100                    row0,
12101                    col0,
12102                    entries,
12103                } => {
12104                    input_cells = input_cells.saturating_add(entries.len());
12105                    let sheet = match located {
12106                        Some((id, sheet)) if id == sheet_id => sheet,
12107                        _ => {
12108                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12109                            located = Some((sheet_id, sheet));
12110                            sheet
12111                        }
12112                    };
12113                    // Rows are located one by one (a binary search, no sheet
12114                    // lookup); the group map is touched once per chunk
12115                    // segment of the run.
12116                    let mut segment: Vec<ComputedWriteChunkEntryPlan> = Vec::new();
12117                    let mut segment_key: Option<ComputedWriteChunkKey> = None;
12118                    for (k, (value, format_id)) in entries.into_iter().enumerate() {
12119                        let row = row0.saturating_add(k as u32) as usize;
12120                        let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12121                            Some(sheet) => {
12122                                Self::locate_row_in_sheet_for_computed_write_plan(sheet, row)
12123                            }
12124                            None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12125                                row,
12126                                32 * 1024,
12127                            ),
12128                        };
12129                        let key = ComputedWriteChunkKey {
12130                            sheet_id,
12131                            col0,
12132                            chunk_idx,
12133                            chunk_start_row0,
12134                        };
12135                        if segment_key != Some(key)
12136                            && let Some(done) = segment_key.replace(key)
12137                        {
12138                            groups.entry(done).or_default().append(&mut segment);
12139                        }
12140                        segment.push(ComputedWriteChunkEntryPlan {
12141                            row_in_chunk,
12142                            seq,
12143                            value,
12144                            format_id,
12145                        });
12146                    }
12147                    if let Some(done) = segment_key {
12148                        groups.entry(done).or_default().append(&mut segment);
12149                    }
12150                }
12151                ComputedWrite::Rect {
12152                    seq,
12153                    sheet_id,
12154                    sr0,
12155                    sc0,
12156                    values,
12157                } => {
12158                    for (r_off, row) in values.into_iter().enumerate() {
12159                        for (c_off, value) in row.into_iter().enumerate() {
12160                            input_cells = input_cells.saturating_add(1);
12161                            self.push_computed_write_plan_entry(
12162                                &mut groups,
12163                                seq,
12164                                sheet_id,
12165                                sr0.saturating_add(r_off as u32),
12166                                sc0.saturating_add(c_off as u32),
12167                                value,
12168                                None,
12169                            );
12170                        }
12171                    }
12172                }
12173            }
12174        }
12175
12176        let mut plan = ComputedWriteCoalescingPlan {
12177            chunks: Vec::with_capacity(groups.len()),
12178            input_cells,
12179            coalesced_cells: 0,
12180            overwritten_cells: 0,
12181        };
12182        for (key, entries) in groups {
12183            let computed_lane_has_formats =
12184                self.computed_overlay_chunk_has_formats(key.sheet_id, key.col0, key.chunk_idx);
12185            let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(
12186                key,
12187                entries,
12188                formats_present,
12189                computed_lane_has_formats,
12190            );
12191            #[cfg(test)]
12192            if matches!(
12193                &chunk_plan.format_effect,
12194                ComputedWriteChunkFormatEffect::SetExplicit(_)
12195            ) {
12196                self.computed_format_vector_allocations_for_test
12197                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12198            }
12199            plan.coalesced_cells = plan
12200                .coalesced_cells
12201                .saturating_add(chunk_plan.entries.len());
12202            plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
12203            plan.chunks.push(chunk_plan);
12204        }
12205        debug_assert_eq!(
12206            plan.input_cells,
12207            plan.coalesced_cells.saturating_add(plan.overwritten_cells)
12208        );
12209        plan
12210    }
12211
12212    fn push_computed_write_plan_entry(
12213        &self,
12214        groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
12215        seq: u64,
12216        sheet_id: SheetId,
12217        row0: u32,
12218        col0: u32,
12219        value: OverlayValue,
12220        format_id: Option<crate::format::FormatId>,
12221    ) {
12222        let (chunk_idx, chunk_start_row0, row_in_chunk) =
12223            self.locate_computed_write_chunk(sheet_id, row0);
12224        let key = ComputedWriteChunkKey {
12225            sheet_id,
12226            col0,
12227            chunk_idx,
12228            chunk_start_row0,
12229        };
12230        groups
12231            .entry(key)
12232            .or_default()
12233            .push(ComputedWriteChunkEntryPlan {
12234                row_in_chunk,
12235                seq,
12236                value,
12237                format_id,
12238            });
12239    }
12240
12241    fn computed_overlay_chunk_has_formats(
12242        &self,
12243        sheet_id: SheetId,
12244        col0: u32,
12245        chunk_idx: usize,
12246    ) -> bool {
12247        let sheet_name = self.graph.sheet_name(sheet_id);
12248        self.arrow_sheets
12249            .sheet(sheet_name)
12250            .and_then(|sheet| sheet.columns.get(col0 as usize))
12251            .and_then(|column| column.chunk(chunk_idx))
12252            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
12253    }
12254
12255    fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
12256        let sheet_name = self.graph.sheet_name(sheet_id);
12257        if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
12258            return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
12259        }
12260        Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
12261    }
12262
12263    fn locate_row_in_sheet_for_computed_write_plan(
12264        sheet: &crate::arrow_store::ArrowSheet,
12265        row0: usize,
12266    ) -> (usize, u32, usize) {
12267        if row0 < sheet.nrows as usize
12268            && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
12269        {
12270            let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
12271            return (chunk_idx, chunk_start as u32, row_in_chunk);
12272        }
12273
12274        let chunk_rows = sheet.chunk_rows.max(1);
12275        if sheet.chunk_starts.is_empty() {
12276            return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
12277        }
12278
12279        let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
12280        let mut chunk_start = sheet.chunk_starts[chunk_idx];
12281        while chunk_start.saturating_add(chunk_rows) <= row0 {
12282            chunk_idx = chunk_idx.saturating_add(1);
12283            chunk_start = chunk_start.saturating_add(chunk_rows);
12284        }
12285        (
12286            chunk_idx,
12287            chunk_start as u32,
12288            row0.saturating_sub(chunk_start),
12289        )
12290    }
12291
12292    fn locate_row_in_empty_sheet_for_computed_write_plan(
12293        row0: usize,
12294        chunk_rows: usize,
12295    ) -> (usize, u32, usize) {
12296        let chunk_rows = chunk_rows.max(1);
12297        let chunk_idx = row0 / chunk_rows;
12298        let chunk_start = chunk_idx.saturating_mul(chunk_rows);
12299        (
12300            chunk_idx,
12301            chunk_start as u32,
12302            row0.saturating_sub(chunk_start),
12303        )
12304    }
12305
12306    #[cfg(test)]
12307    pub(crate) fn debug_plan_computed_write_coalescing(
12308        &self,
12309        buffer: &ComputedWriteBuffer,
12310    ) -> ComputedWriteCoalescingPlan {
12311        self.plan_computed_write_coalescing(buffer)
12312    }
12313
12314    pub(crate) fn flush_computed_write_buffer(
12315        &mut self,
12316        buffer: &mut ComputedWriteBuffer,
12317    ) -> Result<(), ExcelError> {
12318        if buffer.is_empty() {
12319            return Ok(());
12320        }
12321
12322        // Keep ownership of all pending writes until the final request
12323        // checkpoint and bounded commit-window preflight succeed so failures
12324        // remain retry safe. The synchronization and flush that follow are
12325        // infallible mutations with no cancellation point.
12326        self.resource_checkpoint(0)?;
12327        let commit_started = self.preflight_evaluation_commit_window(buffer.len())?;
12328        let (writes, formats_present) = buffer.take_writes();
12329        let plan = self.plan_owned_computed_write_coalescing(writes, formats_present);
12330        self.flush_computed_write_plan(plan);
12331        self.observe_evaluation_commit_window(commit_started);
12332
12333        Ok(())
12334    }
12335
12336    fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
12337        for chunk in plan.chunks {
12338            self.flush_computed_write_chunk_plan(chunk);
12339        }
12340    }
12341
12342    fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
12343        match &chunk.shape {
12344            ComputedWriteChunkPlanShape::Point => {
12345                self.flush_computed_write_chunk_plan_as_points(chunk);
12346            }
12347            ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
12348                self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
12349            }
12350            ComputedWriteChunkPlanShape::DenseRange { .. } => {
12351                self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12352            }
12353            ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
12354                if Self::should_emit_computed_run_fragment(*len, *runs) {
12355                    self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
12356                } else {
12357                    self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12358                }
12359            }
12360        }
12361    }
12362
12363    #[inline]
12364    fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
12365        runs <= len / 2
12366    }
12367
12368    fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
12369        let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
12370        for entry in chunk.entries {
12371            let row0 = chunk
12372                .chunk_start_row0
12373                .saturating_add(entry.row_in_chunk as u32);
12374            self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
12375        }
12376        self.apply_computed_overlay_format_effect(
12377            chunk.sheet_id,
12378            chunk.col0,
12379            chunk.chunk_idx,
12380            chunk.format_effect,
12381        );
12382    }
12383
12384    fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
12385        &mut self,
12386        chunk: ComputedWriteChunkPlan,
12387    ) {
12388        let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
12389        let sheet_id = chunk.sheet_id;
12390        let col0 = chunk.col0;
12391        let chunk_idx = chunk.chunk_idx;
12392        let chunk_start_row0 = chunk.chunk_start_row0;
12393        let format_effect = chunk.format_effect;
12394        let items: Vec<(usize, OverlayValue)> = chunk
12395            .entries
12396            .into_iter()
12397            .map(|entry| (entry.row_in_chunk, entry.value))
12398            .collect();
12399        match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
12400            items,
12401            point_estimate,
12402        ) {
12403            Some(Ok(fragment)) => {
12404                self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
12405            }
12406            Some(Err(cells)) => {
12407                self.flush_computed_overlay_cells_as_points(
12408                    sheet_id,
12409                    col0,
12410                    chunk_start_row0,
12411                    cells,
12412                );
12413            }
12414            None => {}
12415        }
12416        self.apply_computed_overlay_format_effect(sheet_id, col0, chunk_idx, format_effect);
12417    }
12418
12419    #[inline]
12420    fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
12421        chunk
12422            .entries
12423            .iter()
12424            .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
12425            .fold(0usize, usize::saturating_add)
12426    }
12427
12428    fn flush_computed_overlay_cells_as_points(
12429        &mut self,
12430        sheet_id: SheetId,
12431        col0: u32,
12432        chunk_start_row0: u32,
12433        cells: Vec<(usize, OverlayValue)>,
12434    ) {
12435        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12436        for (row_in_chunk, value) in cells {
12437            let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
12438            self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
12439        }
12440    }
12441
12442    fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12443        if chunk.entries.is_empty() {
12444            return;
12445        }
12446        let start = chunk.entries[0].row_in_chunk;
12447        let values: Vec<OverlayValue> =
12448            chunk.entries.into_iter().map(|entry| entry.value).collect();
12449        if let Some(fragment) = OverlayFragment::dense_range(start, values) {
12450            self.apply_computed_overlay_fragment(
12451                chunk.sheet_id,
12452                chunk.col0,
12453                chunk.chunk_idx,
12454                fragment,
12455            );
12456        }
12457        self.apply_computed_overlay_format_effect(
12458            chunk.sheet_id,
12459            chunk.col0,
12460            chunk.chunk_idx,
12461            chunk.format_effect,
12462        );
12463    }
12464
12465    fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12466        if chunk.entries.is_empty() {
12467            return;
12468        }
12469        let start = chunk.entries[0].row_in_chunk;
12470        let values: Vec<OverlayValue> =
12471            chunk.entries.into_iter().map(|entry| entry.value).collect();
12472        if let Some(fragment) = OverlayFragment::run_range(start, values) {
12473            self.apply_computed_overlay_fragment(
12474                chunk.sheet_id,
12475                chunk.col0,
12476                chunk.chunk_idx,
12477                fragment,
12478            );
12479        }
12480        self.apply_computed_overlay_format_effect(
12481            chunk.sheet_id,
12482            chunk.col0,
12483            chunk.chunk_idx,
12484            chunk.format_effect,
12485        );
12486    }
12487
12488    fn apply_computed_overlay_format_effect(
12489        &mut self,
12490        sheet_id: SheetId,
12491        col0: u32,
12492        chunk_idx: usize,
12493        effect: ComputedWriteChunkFormatEffect,
12494    ) {
12495        if !(self.config.arrow_storage_enabled
12496            && self.config.delta_overlay_enabled
12497            && self.config.write_formula_overlay_enabled)
12498            || self.computed_overlay_mirroring_disabled
12499        {
12500            return;
12501        }
12502
12503        let sheet_name = self.graph.sheet_name(sheet_id);
12504        let Some(sheet) = self.arrow_sheets.sheet_mut(sheet_name) else {
12505            return;
12506        };
12507        let Some(chunk) = sheet
12508            .columns
12509            .get_mut(col0 as usize)
12510            .and_then(|column| column.chunk_mut(chunk_idx))
12511        else {
12512            return;
12513        };
12514        match effect {
12515            ComputedWriteChunkFormatEffect::NoFormatWork => {}
12516            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Range {
12517                start,
12518                end,
12519            }) => {
12520                #[cfg(test)]
12521                {
12522                    self.computed_overlay_stale_clear_range_effects_for_test = self
12523                        .computed_overlay_stale_clear_range_effects_for_test
12524                        .saturating_add(1);
12525                }
12526                chunk.computed_overlay.clear_format_range(start, end);
12527            }
12528            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Offsets(
12529                offsets,
12530            )) => {
12531                #[cfg(test)]
12532                {
12533                    self.computed_overlay_stale_clear_offset_attempts_for_test = self
12534                        .computed_overlay_stale_clear_offset_attempts_for_test
12535                        .saturating_add(offsets.len() as u64);
12536                }
12537                chunk.computed_overlay.clear_format_offsets(&offsets);
12538            }
12539            ComputedWriteChunkFormatEffect::SetExplicit(formats) => {
12540                #[cfg(test)]
12541                {
12542                    self.computed_overlay_set_explicit_entry_operations_for_test = self
12543                        .computed_overlay_set_explicit_entry_operations_for_test
12544                        .saturating_add(formats.len() as u64);
12545                }
12546                for (row_in_chunk, format_id) in formats {
12547                    chunk.computed_overlay.set_format(row_in_chunk, format_id);
12548                }
12549            }
12550        }
12551    }
12552
12553    fn apply_computed_overlay_fragment(
12554        &mut self,
12555        sheet_id: SheetId,
12556        col0: u32,
12557        chunk_idx: usize,
12558        fragment: OverlayFragment,
12559    ) {
12560        if !(self.config.arrow_storage_enabled
12561            && self.config.delta_overlay_enabled
12562            && self.config.write_formula_overlay_enabled)
12563        {
12564            return;
12565        }
12566        if self.computed_overlay_mirroring_disabled {
12567            return;
12568        }
12569
12570        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12571        self.ensure_arrow_sheet(&sheet_name);
12572
12573        let col0 = col0 as usize;
12574        let asheet = self
12575            .arrow_sheets
12576            .sheet_mut(&sheet_name)
12577            .expect("ArrowSheet must exist");
12578
12579        let cur_cols = asheet.columns.len();
12580        if col0 >= cur_cols {
12581            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12582        }
12583
12584        let start_row0 = asheet
12585            .chunk_starts
12586            .get(chunk_idx)
12587            .copied()
12588            .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
12589        let required_rows =
12590            start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
12591        if required_rows > asheet.nrows as usize {
12592            if asheet.columns.is_empty() {
12593                asheet.insert_columns(0, 1);
12594            }
12595            asheet.ensure_row_capacity(required_rows);
12596        }
12597
12598        let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
12599            return;
12600        };
12601        let delta = ch.computed_overlay.apply_fragment(fragment);
12602        self.adjust_computed_overlay_bytes(delta);
12603
12604        if let Some(cap) = self.config.max_overlay_memory_bytes
12605            && self.computed_overlay_bytes_estimate > cap
12606        {
12607            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12608        }
12609    }
12610
12611    #[inline]
12612    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
12613        if delta >= 0 {
12614            self.computed_overlay_bytes_estimate = self
12615                .computed_overlay_bytes_estimate
12616                .saturating_add(delta as usize);
12617        } else {
12618            self.computed_overlay_bytes_estimate = self
12619                .computed_overlay_bytes_estimate
12620                .saturating_sub((-delta) as usize);
12621        }
12622    }
12623
12624    fn clear_all_computed_overlays(&mut self) {
12625        let mut freed_total = 0usize;
12626        for sh in self.arrow_sheets.sheets.iter_mut() {
12627            for col in sh.columns.iter_mut() {
12628                for ch in col.chunks.iter_mut() {
12629                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12630                }
12631                for ch in col.sparse_chunks.values_mut() {
12632                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12633                }
12634            }
12635        }
12636        self.computed_overlay_bytes_estimate = self
12637            .computed_overlay_bytes_estimate
12638            .saturating_sub(freed_total);
12639    }
12640
12641    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
12642        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
12643        // Handle budget pressure by compacting computed overlays into base lanes.
12644        self.compact_all_computed_overlays();
12645    }
12646
12647    /// Fold all computed overlay entries across all sheets into their base arrays.
12648    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
12649    fn compact_all_computed_overlays(&mut self) {
12650        let mut freed_total = 0usize;
12651        for sheet in self.arrow_sheets.sheets.iter_mut() {
12652            for col_idx in 0..sheet.columns.len() {
12653                // Dense chunks
12654                let num_dense = sheet.columns[col_idx].chunks.len();
12655                for ch_idx in 0..num_dense {
12656                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
12657                }
12658                // Sparse chunks
12659                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
12660                    .sparse_chunks
12661                    .keys()
12662                    .copied()
12663                    .collect();
12664                for ch_idx in sparse_keys {
12665                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
12666                }
12667            }
12668        }
12669        self.computed_overlay_bytes_estimate = self
12670            .computed_overlay_bytes_estimate
12671            .saturating_sub(freed_total);
12672        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
12673    }
12674
12675    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
12676        let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
12677    }
12678
12679    fn record_vertex_value_to_overlay(
12680        &mut self,
12681        vertex_id: VertexId,
12682        value: &LiteralValue,
12683        computed_writes: Option<&mut ComputedWriteBuffer>,
12684    ) -> Result<(), ExcelError> {
12685        if !(self.config.arrow_storage_enabled
12686            && self.config.delta_overlay_enabled
12687            && self.config.write_formula_overlay_enabled)
12688        {
12689            return Ok(());
12690        }
12691        if self.computed_overlay_mirroring_disabled {
12692            return Ok(());
12693        }
12694        if !matches!(
12695            self.graph.get_vertex_kind(vertex_id),
12696            VertexKind::FormulaScalar | VertexKind::FormulaArray
12697        ) {
12698            return Ok(());
12699        }
12700        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
12701            return Ok(());
12702        };
12703        let Some(buffer) = computed_writes else {
12704            // Unbuffered: one sheet lookup for the value and its format.
12705            let format_id = self.derived_formats.get(&cell);
12706            self.write_computed_cell_0based(cell, value, format_id);
12707            return Ok(());
12708        };
12709        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
12710        let date_system = self.arrow_sheet_date_system(&sheet_name);
12711        let ov = Self::literal_to_overlay_value(value, date_system);
12712        {
12713            let format_id = self.derived_formats.get(&cell);
12714            buffer.push_cell_with_format(
12715                cell.sheet_id,
12716                cell.coord.row(),
12717                cell.coord.col(),
12718                ov,
12719                format_id,
12720            );
12721            if self.should_flush_computed_write_buffer(buffer) {
12722                self.flush_computed_write_buffer(buffer)?;
12723            }
12724        }
12725        Ok(())
12726    }
12727
12728    #[inline]
12729    fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
12730        self.config.max_overlay_memory_bytes.is_some_and(|cap| {
12731            if cap == 0 {
12732                return false;
12733            }
12734            self.computed_overlay_bytes_estimate
12735                .saturating_add(buffer.estimated_bytes())
12736                > cap
12737        })
12738    }
12739
12740    /// Estimated memory usage for computed overlays (formula/spill mirroring).
12741    pub fn overlay_memory_usage(&self) -> usize {
12742        self.computed_overlay_bytes_estimate
12743    }
12744
12745    #[cfg(test)]
12746    pub(crate) fn debug_overlay_compactions(&self) -> u64 {
12747        self.overlay_compactions
12748    }
12749
12750    #[cfg(test)]
12751    pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
12752        let mut total = 0usize;
12753        for sheet in &self.arrow_sheets.sheets {
12754            for column in &sheet.columns {
12755                for chunk in &column.chunks {
12756                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
12757                }
12758                for chunk in column.sparse_chunks.values() {
12759                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
12760                }
12761            }
12762        }
12763        self.computed_overlay_bytes_estimate = total;
12764        total
12765    }
12766
12767    fn resolve_sheet_locator_for_write(
12768        &mut self,
12769        loc: formualizer_common::SheetLocator<'_>,
12770        current_sheet: &str,
12771    ) -> Result<SheetId, ExcelError> {
12772        Ok(match loc {
12773            formualizer_common::SheetLocator::Id(id) => id,
12774            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
12775            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
12776        })
12777    }
12778
12779    fn resolve_sheet_locator_for_read(
12780        &self,
12781        loc: formualizer_common::SheetLocator<'_>,
12782        current_sheet: &str,
12783    ) -> Result<SheetId, ExcelError> {
12784        match loc {
12785            formualizer_common::SheetLocator::Id(id) => Ok(id),
12786            formualizer_common::SheetLocator::Name(name) => self
12787                .graph
12788                .sheet_id(name.as_ref())
12789                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
12790            formualizer_common::SheetLocator::Current => self
12791                .graph
12792                .sheet_id(current_sheet)
12793                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
12794        }
12795    }
12796
12797    /// Set a cell value
12798    pub fn set_cell_value(
12799        &mut self,
12800        sheet: &str,
12801        row: u32,
12802        col: u32,
12803        value: LiteralValue,
12804    ) -> Result<(), ExcelError> {
12805        self.observe_function_semantic_epoch()?;
12806        let sheet_existed = self.graph.sheet_id(sheet).is_some();
12807        let sheet_id = self.graph.sheet_id_mut(sheet);
12808        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
12809        let replaced_formula =
12810            self.graph
12811                .get_vertex_id_for_address(&cell_ref)
12812                .is_some_and(|vertex| {
12813                    matches!(
12814                        self.graph.get_vertex_kind(vertex),
12815                        VertexKind::FormulaScalar | VertexKind::FormulaArray
12816                    )
12817                });
12818        self.graph.set_cell_value(sheet, row, col, value.clone())?;
12819        self.clear_cell_format_state(sheet, cell_ref);
12820        self.record_changed_cell(sheet, row, col);
12821        if !sheet_existed || replaced_formula {
12822            self.mark_topology_edited();
12823        }
12824        // Mirror into Arrow overlay when enabled
12825        self.mirror_value_to_overlay(sheet, row, col, &value);
12826        // Advance snapshot to reflect external mutation.
12827        self.mark_data_edited();
12828        Ok(())
12829    }
12830
12831    /// Record a single-cell change: invalidates pending spills blocked on it.
12832    fn record_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
12833        let sheet_id = self.graph.sheet_id_mut(sheet);
12834        self.record_structural_change(StructuralScope::Cell {
12835            sheet: sheet_id,
12836            row: row.saturating_sub(1),
12837            col: col.saturating_sub(1),
12838        });
12839    }
12840
12841    fn record_change_for_event(&mut self, event: &ChangeEvent) {
12842        match event {
12843            ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
12844                self.record_structural_change(StructuralScope::Cell {
12845                    sheet: addr.sheet_id,
12846                    row: addr.coord.row(),
12847                    col: addr.coord.col(),
12848                });
12849            }
12850            ChangeEvent::SpillCommitted { new, .. } => {
12851                if let Some(scope) = Self::structural_scope_from_cells(&new.target_cells) {
12852                    self.record_structural_change(scope);
12853                }
12854            }
12855            ChangeEvent::SpillCleared { old, .. } => {
12856                if let Some(scope) = Self::structural_scope_from_cells(&old.target_cells) {
12857                    self.record_structural_change(scope);
12858                }
12859            }
12860            ChangeEvent::DefineName { .. }
12861            | ChangeEvent::UpdateName { .. }
12862            | ChangeEvent::DeleteName { .. }
12863            | ChangeEvent::NamedRangeAdjusted { .. } => {
12864                // Direct name events are preflighted by the logged-name APIs.
12865                // Structural entry points preflight spans before emitting a
12866                // NamedRangeAdjusted event. Epoch changes only rebuild caches.
12867                self.record_structural_change(StructuralScope::AllSheets);
12868            }
12869            ChangeEvent::VertexMoved { .. } | ChangeEvent::FormulaAdjusted { .. } => {
12870                // Structural entry points publish their axis delta once after
12871                // the graph and Arrow commits.
12872            }
12873            ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
12874                self.record_structural_change(StructuralScope::Region(Region::whole_row(
12875                    *sheet_id, *row0,
12876                )));
12877            }
12878            ChangeEvent::AddVertex { .. }
12879            | ChangeEvent::RemoveVertex { .. }
12880            | ChangeEvent::EdgeAdded { .. }
12881            | ChangeEvent::EdgeRemoved { .. }
12882            | ChangeEvent::CompoundStart { .. }
12883            | ChangeEvent::CompoundEnd { .. }
12884            | ChangeEvent::StagedFormulaCellChanged { .. } => {}
12885        }
12886    }
12887
12888    fn record_structural_change(&mut self, scope: StructuralScope) {
12889        self.invalidate_pending_spills(scope);
12890    }
12891
12892    fn structural_scope_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
12893        let first = cells.first()?;
12894        let sheet_id = first.sheet_id;
12895        if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
12896            return Some(StructuralScope::OpaqueGlobal);
12897        }
12898        let mut row_start = first.coord.row();
12899        let mut row_end = row_start;
12900        let mut col_start = first.coord.col();
12901        let mut col_end = col_start;
12902        for cell in cells.iter().skip(1) {
12903            row_start = row_start.min(cell.coord.row());
12904            row_end = row_end.max(cell.coord.row());
12905            col_start = col_start.min(cell.coord.col());
12906            col_end = col_end.max(cell.coord.col());
12907        }
12908        Some(StructuralScope::Region(Region::rect(
12909            sheet_id, row_start, row_end, col_start, col_end,
12910        )))
12911    }
12912
12913    pub fn set_cell_value_ref(
12914        &mut self,
12915        cell: formualizer_common::SheetCellRef<'_>,
12916        current_sheet: &str,
12917        value: LiteralValue,
12918    ) -> Result<(), ExcelError> {
12919        let owned = cell.into_owned();
12920        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
12921        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12922        self.set_cell_value(
12923            &sheet_name,
12924            owned.coord.row() + 1,
12925            owned.coord.col() + 1,
12926            value,
12927        )
12928    }
12929
12930    pub fn set_cell_formula_ref(
12931        &mut self,
12932        cell: formualizer_common::SheetCellRef<'_>,
12933        current_sheet: &str,
12934        ast: ASTNode,
12935    ) -> Result<(), ExcelError> {
12936        let owned = cell.into_owned();
12937        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
12938        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12939        self.set_cell_formula(
12940            &sheet_name,
12941            owned.coord.row() + 1,
12942            owned.coord.col() + 1,
12943            ast,
12944        )
12945    }
12946
12947    pub fn get_cell_value_ref(
12948        &self,
12949        cell: formualizer_common::SheetCellRef<'_>,
12950        current_sheet: &str,
12951    ) -> Result<Option<LiteralValue>, ExcelError> {
12952        let owned = cell.into_owned();
12953        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
12954        let sheet_name = self.graph.sheet_name(sheet_id);
12955        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
12956    }
12957
12958    pub fn resolve_range_view_sheet_ref<'c>(
12959        &'c self,
12960        r: &formualizer_common::SheetRef<'_>,
12961        current_sheet: &str,
12962    ) -> Result<RangeView<'c>, ExcelError> {
12963        use formualizer_common::SheetLocator;
12964
12965        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
12966            match loc {
12967                SheetLocator::Current => Ok(None),
12968                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
12969                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
12970            }
12971        };
12972
12973        let rt = match r {
12974            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
12975                sheet: sheet_to_opt_name(cell.sheet.clone())?,
12976                row: cell.coord.row() + 1,
12977                col: cell.coord.col() + 1,
12978                row_abs: cell.coord.row_abs(),
12979                col_abs: cell.coord.col_abs(),
12980            },
12981            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
12982                sheet: sheet_to_opt_name(range.sheet.clone())?,
12983                start_row: range.start_row.map(|b| b.index + 1),
12984                start_col: range.start_col.map(|b| b.index + 1),
12985                end_row: range.end_row.map(|b| b.index + 1),
12986                end_col: range.end_col.map(|b| b.index + 1),
12987                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
12988                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
12989                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
12990                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
12991            },
12992        };
12993
12994        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
12995    }
12996
12997    /// Set a cell formula
12998    pub fn set_cell_formula(
12999        &mut self,
13000        sheet: &str,
13001        row: u32,
13002        col: u32,
13003        ast: ASTNode,
13004    ) -> Result<(), ExcelError> {
13005        self.observe_function_semantic_epoch()?;
13006        let sheet_id = self.graph.sheet_id_mut(sheet);
13007        let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13008        let ingested = {
13009            let mut pipeline = self.ingest_pipeline();
13010            pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
13011        };
13012        self.graph.set_cell_formula_with_plan(
13013            sheet,
13014            row,
13015            col,
13016            ingested.ast_id,
13017            &ingested.dep_plan,
13018            ingested.dep_plan.volatile,
13019            ingested.dep_plan.dynamic,
13020        )?;
13021        self.clear_cell_format_state(sheet, placement);
13022        self.record_changed_cell(sheet, row, col);
13023
13024        // If the cell previously held a user value in the delta overlay, it must not continue
13025        // to mask the formula result under Arrow-canonical reads (overlay precedence is
13026        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
13027        // because an explicit `Empty` overlay would still take precedence over computed values.
13028        self.clear_delta_overlay_cell(sheet, row, col);
13029
13030        // Advance snapshot to reflect external mutation
13031        self.mark_topology_edited();
13032        Ok(())
13033    }
13034
13035    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
13036    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
13037    where
13038        I: IntoIterator<Item = (u32, u32, ASTNode)>,
13039    {
13040        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
13041        let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
13042        let sheet_id = self.graph.sheet_id_mut(sheet);
13043        let ingested = {
13044            let mut pipeline = self.ingest_pipeline();
13045            let inputs = collected.into_iter().map(|(row, col, ast)| {
13046                let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13047                (FormulaAstInput::Tree(ast), placement, None)
13048            });
13049            pipeline.ingest_batch(inputs)?
13050        };
13051        let planned: Vec<(u32, u32, AstNodeId, DependencyPlanRow)> = ingested
13052            .into_iter()
13053            .map(|formula| {
13054                (
13055                    formula.placement.coord.row() + 1,
13056                    formula.placement.coord.col() + 1,
13057                    formula.ast_id,
13058                    formula.dep_plan,
13059                )
13060            })
13061            .collect();
13062        let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
13063        for (row, col) in edited_cells {
13064            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13065            self.clear_cell_format_state(sheet, cell);
13066            self.record_changed_cell(sheet, row, col);
13067        }
13068        // Single topology bump after batch
13069        if n > 0 {
13070            self.mark_topology_edited();
13071        }
13072        Ok(n)
13073    }
13074
13075    #[inline]
13076    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
13077        match v {
13078            LiteralValue::Empty => None,
13079            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
13080            other => Some(other),
13081        }
13082    }
13083
13084    fn materialize_temporal_egress(
13085        value: LiteralValue,
13086        class: Option<&formualizer_common::numfmt::FormatClass>,
13087        policy: crate::engine::TemporalEgress,
13088        date_system: crate::engine::DateSystem,
13089    ) -> LiteralValue {
13090        use formualizer_common::numfmt::FormatClass;
13091        if policy == crate::engine::TemporalEgress::Serial {
13092            return value;
13093        }
13094        let LiteralValue::Number(serial) = value else {
13095            return value;
13096        };
13097        match class {
13098            Some(FormatClass::Date) => {
13099                formualizer_common::try_serial_to_date_for(date_system, serial)
13100                    .map(LiteralValue::Date)
13101                    .unwrap_or(LiteralValue::Number(serial))
13102            }
13103            Some(FormatClass::DateTime) => {
13104                formualizer_common::try_serial_to_datetime_for(date_system, serial)
13105                    .map(LiteralValue::DateTime)
13106                    .unwrap_or(LiteralValue::Number(serial))
13107            }
13108            Some(FormatClass::Time) => {
13109                let seconds = (serial.rem_euclid(1.0) * 86_400.0).round() as u32 % 86_400;
13110                chrono::NaiveTime::from_num_seconds_from_midnight_opt(seconds, 0)
13111                    .map(LiteralValue::Time)
13112                    .unwrap_or(LiteralValue::Number(serial))
13113            }
13114            Some(FormatClass::Duration) => {
13115                let nanos = (serial * 86_400.0 * 1_000_000_000.0).round();
13116                if nanos.is_finite() && nanos >= i64::MIN as f64 && nanos <= i64::MAX as f64 {
13117                    LiteralValue::Duration(chrono::Duration::nanoseconds(nanos as i64))
13118                } else {
13119                    LiteralValue::Number(serial)
13120                }
13121            }
13122            _ => LiteralValue::Number(serial),
13123        }
13124    }
13125
13126    pub(crate) fn effective_format_id(
13127        &self,
13128        sheet: &str,
13129        row: u32,
13130        col: u32,
13131    ) -> Option<crate::format::FormatId> {
13132        let arrow = self.arrow_sheets.sheet(sheet).and_then(|arrow| {
13133            arrow.format_id(
13134                row.saturating_sub(1) as usize,
13135                col.saturating_sub(1) as usize,
13136            )
13137        });
13138        arrow.or_else(|| {
13139            let sheet_id = self.graph.sheet_id(sheet)?;
13140            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13141            self.derived_formats.get(&cell)
13142        })
13143    }
13144
13145    /// Get a cell value through the single temporal egress boundary.
13146    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13147        let raw = self.read_cell_value(sheet, row, col)?;
13148        let format = self.effective_format_id(sheet, row, col);
13149        let class = format.and_then(|id| self.format_registry.class(id));
13150        Self::normalize_public_cell_read(Self::materialize_temporal_egress(
13151            raw,
13152            class,
13153            self.config.temporal_egress,
13154            self.config.date_system,
13155        ))
13156    }
13157
13158    /// Read a rectangular range through the temporal egress boundary.
13159    pub fn get_range_values(
13160        &self,
13161        sheet: &str,
13162        sr: u32,
13163        sc: u32,
13164        er: u32,
13165        ec: u32,
13166    ) -> Vec<Vec<LiteralValue>> {
13167        let height = er.saturating_sub(sr).saturating_add(1) as usize;
13168        let width = ec.saturating_sub(sc).saturating_add(1) as usize;
13169        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13170            return vec![vec![LiteralValue::Empty; width]; height];
13171        };
13172        let view = asheet.range_view(
13173            sr.saturating_sub(1) as usize,
13174            sc.saturating_sub(1) as usize,
13175            er.saturating_sub(1) as usize,
13176            ec.saturating_sub(1) as usize,
13177        );
13178        let sheet_id = self.graph.sheet_id(sheet);
13179        let derived_formats = &self.derived_formats;
13180        let has_derived_formats =
13181            sheet_id.is_some_and(|sheet_id| derived_formats.any(|cell| cell.sheet_id == sheet_id));
13182        let mut out = Vec::with_capacity(height);
13183        if !asheet.has_formats() && !has_derived_formats {
13184            for rr in 0..height {
13185                let mut row = Vec::with_capacity(width);
13186                for cc in 0..width {
13187                    row.push(view.get_cell(rr, cc));
13188                }
13189                out.push(row);
13190            }
13191            return out;
13192        }
13193        let format_registry = &self.format_registry;
13194        for rr in 0..height {
13195            let mut row = Vec::with_capacity(width);
13196            for cc in 0..width {
13197                let raw = view.get_cell(rr, cc);
13198                let row0 = sr.saturating_sub(1).saturating_add(rr as u32);
13199                let col0 = sc.saturating_sub(1).saturating_add(cc as u32);
13200                let format = asheet.format_id(row0 as usize, col0 as usize).or_else(|| {
13201                    let cell = CellRef::new(sheet_id?, Coord::new(row0, col0, true, true));
13202                    derived_formats.get(&cell)
13203                });
13204                let class = format.and_then(|id| format_registry.class(id));
13205                row.push(Self::materialize_temporal_egress(
13206                    raw,
13207                    class,
13208                    self.config.temporal_egress,
13209                    self.config.date_system,
13210                ));
13211            }
13212            out.push(row);
13213        }
13214        out
13215    }
13216
13217    /// Unified internal read API for a single cell value (Arrow-truth).
13218    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13219        let asheet = self.sheet_store().sheet(sheet)?;
13220        let r0 = row.saturating_sub(1) as usize;
13221        let c0 = col.saturating_sub(1) as usize;
13222        let v = asheet.get_cell_value(r0, c0);
13223        if matches!(v, LiteralValue::Empty) {
13224            None
13225        } else {
13226            Some(v)
13227        }
13228    }
13229
13230    /// Unified internal read API for a range of cell values (Arrow-truth).
13231    pub(crate) fn read_range_values(
13232        &self,
13233        sheet: &str,
13234        sr: u32,
13235        sc: u32,
13236        er: u32,
13237        ec: u32,
13238    ) -> RangeView<'_> {
13239        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13240            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
13241        };
13242        if er < sr || ec < sc {
13243            return asheet.range_view(1, 1, 0, 0);
13244        }
13245        let sr0 = sr.saturating_sub(1) as usize;
13246        let sc0 = sc.saturating_sub(1) as usize;
13247        let er0 = er.saturating_sub(1) as usize;
13248        let ec0 = ec.saturating_sub(1) as usize;
13249        asheet.range_view(sr0, sc0, er0, ec0)
13250    }
13251
13252    /// Get formula AST (if any) and current stored value for a cell
13253    pub fn get_cell(
13254        &self,
13255        sheet: &str,
13256        row: u32,
13257        col: u32,
13258    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
13259        let v = self.get_cell_value(sheet, row, col);
13260        let sheet_id = self.graph.sheet_id(sheet)?;
13261        let coord = Coord::from_excel(row, col, true, true);
13262        let cell = CellRef::new(sheet_id, coord);
13263        if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
13264            let ast = self.graph.get_formula(vid);
13265            Some((ast, v))
13266        } else if v.is_some() || self.graph.had_legacy_cell_vertex(&cell) {
13267            // A referenced or emptied value cell has no vertex (decision
13268            // 27), but it is a cell the graph knows, as it was when it had
13269            // one (the interactive formula edit routes on this).
13270            Some((None, v))
13271        } else {
13272            None
13273        }
13274    }
13275
13276    /// Begin batch operations - defer CSR rebuilds for better performance
13277    pub fn begin_batch(&mut self) {
13278        self.graph.begin_batch();
13279    }
13280
13281    /// End batch operations and trigger CSR rebuild
13282    pub fn end_batch(&mut self) {
13283        self.graph.end_batch();
13284    }
13285
13286    /// Begin a deferred-dirty scope for a multi-edit batch: while active,
13287    /// every edit's dirty propagation queues its sources instead of running
13288    /// a full BFS per edit, and the outermost `end_deferred_dirty` flushes
13289    /// the union with ONE multi-source propagation (O(component) instead of
13290    /// O(edits × component)). See `DependencyGraph::begin_deferred_dirty`.
13291    ///
13292    /// Callers MUST run `end_deferred_dirty` on every exit path, including
13293    /// error returns; evaluation entry points `debug_assert` no scope leaked.
13294    pub fn begin_deferred_dirty(&mut self) {
13295        self.graph.begin_deferred_dirty();
13296    }
13297
13298    /// End a deferred-dirty scope, flushing the queued propagation when the
13299    /// outermost scope closes. See `Engine::begin_deferred_dirty`.
13300    pub fn end_deferred_dirty(&mut self) {
13301        let _ = self.graph.end_deferred_dirty();
13302    }
13303
13304    /// Total vertices processed by dirty-propagation BFS loops since graph
13305    /// creation. Perf-shape observability only (cross-crate tests assert
13306    /// batched edits propagate O(component), not O(edits × component)).
13307    pub fn dirty_propagation_visits(&self) -> u64 {
13308        self.graph.dirty_propagation_visits()
13309    }
13310
13311    /// Evaluate a single vertex.
13312    /// This is the core of the sequential evaluation logic for Milestone 3.1.
13313    #[inline]
13314    fn record_cell_if_changed(
13315        delta: &mut DeltaCollector,
13316        cell: &CellRef,
13317        old: &LiteralValue,
13318        new: &LiteralValue,
13319    ) {
13320        if old != new {
13321            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
13322        }
13323    }
13324
13325    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
13326        self.observe_evaluation_resource_request(EvaluationRequestKind::Vertex, |engine| {
13327            engine.observe_function_semantic_epoch()?;
13328            // A direct request selects exactly one vertex, regardless of its formula kind.
13329            engine.resource_checkpoint(1)?;
13330            if !engine.graph.vertex_exists(vertex_id) {
13331                return engine.evaluate_vertex_impl(vertex_id, None);
13332            }
13333            let is_formula = matches!(
13334                engine.graph.get_vertex_kind(vertex_id),
13335                VertexKind::FormulaScalar | VertexKind::FormulaArray
13336            );
13337            if is_formula {
13338                engine.begin_evaluation_request();
13339                #[cfg(any(test, feature = "legacy_oracle"))]
13340                engine.graph.flush_pending_edge_deltas();
13341                let roots = [crate::engine::target_preparation::TargetProducer::Legacy(
13342                    vertex_id,
13343                )];
13344                engine.evaluate_legacy_target_roots(&roots, None)?;
13345            }
13346            engine.evaluate_vertex_impl(vertex_id, None)
13347        })
13348    }
13349
13350    /// Same rejection publication for owned arrays and pre-admitted range views.
13351    fn publish_oversized_spill(
13352        &mut self,
13353        vertex_id: VertexId,
13354        error: ExcelError,
13355        mut delta: Option<&mut DeltaCollector>,
13356    ) -> LiteralValue {
13357        self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
13358        let anchor = self
13359            .graph
13360            .get_cell_ref(vertex_id)
13361            .expect("cell ref for vertex");
13362        let spill_val = LiteralValue::Error(error);
13363        if let Some(d) = delta {
13364            let old = self
13365                .read_cell_value(
13366                    self.graph.sheet_name(anchor.sheet_id),
13367                    anchor.coord.row() + 1,
13368                    anchor.coord.col() + 1,
13369                )
13370                .unwrap_or(LiteralValue::Empty);
13371            if old != spill_val {
13372                d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
13373            }
13374        }
13375        self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13376        if self.config.arrow_storage_enabled
13377            && self.config.delta_overlay_enabled
13378            && self.config.write_formula_overlay_enabled
13379        {
13380            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13381            self.mirror_value_to_computed_overlay(
13382                &sheet_name,
13383                anchor.coord.row() + 1,
13384                anchor.coord.col() + 1,
13385                &spill_val,
13386            );
13387        }
13388        spill_val
13389    }
13390
13391    fn evaluate_vertex_impl(
13392        &mut self,
13393        vertex_id: VertexId,
13394        delta: Option<&mut DeltaCollector>,
13395    ) -> Result<LiteralValue, ExcelError> {
13396        // Preserve the direct evaluator's compatibility behavior for invalid IDs, literal cells,
13397        // names, and other non-formula vertices. Only formula publication needs the C1a final
13398        // deadline checkpoint and effects pipeline.
13399        if !self.graph.vertex_exists(vertex_id) {
13400            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
13401                .with_message(format!("Vertex not found: {vertex_id:?}")));
13402        }
13403        if self.active_resource_ledger.is_some()
13404            && matches!(
13405                self.graph.get_vertex_kind(vertex_id),
13406                VertexKind::FormulaScalar | VertexKind::FormulaArray
13407            )
13408        {
13409            let value = self
13410                .evaluate_vertex_immutable(vertex_id)
13411                .unwrap_or_else(LiteralValue::Error);
13412            let effects = self.plan_vertex_effects(vertex_id, value.clone(), None)?;
13413            // Do not publish the selected result until the outer request's deadline succeeds.
13414            self.resource_checkpoint(0)?;
13415            let mut delta = delta;
13416            for effect in &effects {
13417                self.apply_effect_with_computed_writes(effect, delta.as_deref_mut(), None, None)?;
13418            }
13419            return Ok(value);
13420        }
13421
13422        let mut delta = delta;
13423
13424        // Get vertex kind and check if it needs evaluation
13425        let kind = self.graph.get_vertex_kind(vertex_id);
13426        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
13427
13428        let view = match kind {
13429            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
13430                if let Some(view) = self.graph.formula_view(vertex_id) {
13431                    view
13432                } else {
13433                    return Ok(LiteralValue::Number(0.0));
13434                }
13435            }
13436            VertexKind::Empty | VertexKind::Cell => {
13437                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
13438                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13439                    let row = cell_ref.coord.row() + 1;
13440                    let col = cell_ref.coord.col() + 1;
13441                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
13442                        return Ok(v);
13443                    }
13444                }
13445                return Ok(LiteralValue::Number(0.0));
13446            }
13447            VertexKind::NamedScalar => {
13448                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
13449                return Ok(value);
13450            }
13451            VertexKind::NamedArray => {
13452                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
13453                return Ok(value);
13454            }
13455            VertexKind::InfiniteRange
13456            | VertexKind::Range
13457            | VertexKind::External
13458            | VertexKind::Table => {
13459                // Not directly evaluatable here.
13460                return Ok(LiteralValue::Number(0.0));
13461            }
13462        };
13463
13464        // The interpreter uses a reference to the engine as the context.
13465        let sheet_name = self.graph.sheet_name(sheet_id);
13466        let cell_ref = self
13467            .graph
13468            .get_cell_ref(vertex_id)
13469            .expect("cell ref for vertex");
13470        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13471
13472        let result = interpreter.evaluate_formula_view(
13473            view,
13474            self.graph.data_store(),
13475            self.graph.sheet_reg(),
13476        );
13477
13478        // If array result, perform spill from the anchor cell
13479        match result {
13480            Ok(cv) => {
13481                let derived_format = cv.format_id();
13482                self.record_derived_format(vertex_id, derived_format);
13483                let oversized_range = crate::engine::result_finalization::range_spill_error(
13484                    &cv,
13485                    self.config.spill.max_spill_cells,
13486                );
13487                let is_oversized_range = oversized_range.is_some();
13488                let result_literal = if let Some(error) = oversized_range {
13489                    drop(cv);
13490                    LiteralValue::Error(error)
13491                } else {
13492                    crate::engine::result_finalization::finalize_formula_result(cv.into_literal())
13493                };
13494                let output_sheet_name = sheet_name.to_string();
13495                self.write_computed_overlay_format_0based(
13496                    &output_sheet_name,
13497                    cell_ref.coord.row(),
13498                    cell_ref.coord.col(),
13499                    derived_format,
13500                );
13501                if is_oversized_range {
13502                    let LiteralValue::Error(error) = result_literal else {
13503                        unreachable!()
13504                    };
13505                    self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
13506                    return Ok(self.publish_oversized_spill(
13507                        vertex_id,
13508                        error,
13509                        delta.as_deref_mut(),
13510                    ));
13511                }
13512                match result_literal {
13513                    LiteralValue::Array(rows) => {
13514                        // Update kind to FormulaArray for tracking
13515                        self.graph
13516                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
13517                        // Build target cells rectangle starting from anchor
13518                        let anchor = self
13519                            .graph
13520                            .get_cell_ref(vertex_id)
13521                            .expect("cell ref for vertex");
13522                        let sheet_id = anchor.sheet_id;
13523                        let h = rows.len() as u32;
13524                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
13525
13526                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
13527                        let spill_cells = (h as u64).saturating_mul(w as u64);
13528                        if spill_cells > self.config.spill.max_spill_cells as u64 {
13529                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13530                                .with_message("SpillTooLarge")
13531                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13532                                    expected_rows: h,
13533                                    expected_cols: w,
13534                                });
13535                            return Ok(self.publish_oversized_spill(
13536                                vertex_id,
13537                                spill_err,
13538                                delta.as_deref_mut(),
13539                            ));
13540                        }
13541                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
13542                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
13543                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
13544                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
13545                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
13546                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
13547                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
13548                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13549                                .with_message("Spill exceeds sheet bounds")
13550                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13551                                    expected_rows: h,
13552                                    expected_cols: w,
13553                                });
13554                            let spill_val = LiteralValue::Error(spill_err.clone());
13555                            if let Some(d) = delta.as_deref_mut() {
13556                                let old = self
13557                                    .read_cell_value(
13558                                        self.graph.sheet_name(anchor.sheet_id),
13559                                        anchor.coord.row() + 1,
13560                                        anchor.coord.col() + 1,
13561                                    )
13562                                    .unwrap_or(LiteralValue::Empty);
13563                                if old != spill_val {
13564                                    d.record_cell(
13565                                        anchor.sheet_id,
13566                                        anchor.coord.row(),
13567                                        anchor.coord.col(),
13568                                    );
13569                                }
13570                            }
13571                            self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13572                            if self.config.arrow_storage_enabled
13573                                && self.config.delta_overlay_enabled
13574                                && self.config.write_formula_overlay_enabled
13575                            {
13576                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13577                                self.mirror_value_to_computed_overlay(
13578                                    &sheet_name,
13579                                    anchor.coord.row() + 1,
13580                                    anchor.coord.col() + 1,
13581                                    &spill_val,
13582                                );
13583                            }
13584                            return Ok(spill_val);
13585                        }
13586                        let mut targets = Vec::new();
13587                        for r in 0..h {
13588                            for c in 0..w {
13589                                targets.push(self.graph.make_cell_ref_internal(
13590                                    sheet_id,
13591                                    anchor.coord.row() + r,
13592                                    anchor.coord.col() + c,
13593                                ));
13594                            }
13595                        }
13596
13597                        // Plan spill via spill manager shim
13598                        match self.spill_mgr.reserve(
13599                            vertex_id,
13600                            anchor,
13601                            SpillShape { rows: h, cols: w },
13602                            SpillMeta {
13603                                epoch: self.recalc_epoch,
13604                                config: self.config.spill,
13605                            },
13606                        ) {
13607                            Ok(()) => {
13608                                // Commit: write values to grid
13609                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
13610                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
13611                                if let Err(e) = self.commit_spill_and_mirror(
13612                                    vertex_id,
13613                                    &targets,
13614                                    rows.clone(),
13615                                    delta.as_deref_mut(),
13616                                    None,
13617                                ) {
13618                                    if e.kind != ExcelErrorKind::Spill {
13619                                        return Err(e);
13620                                    }
13621                                    // If commit fails, mark as error
13622                                    self.clear_spill_projection_and_mirror(
13623                                        vertex_id,
13624                                        delta.as_deref_mut(),
13625                                    );
13626                                    if let Some(d) = delta.as_deref_mut() {
13627                                        let old = self
13628                                            .read_cell_value(
13629                                                self.graph.sheet_name(anchor.sheet_id),
13630                                                anchor.coord.row() + 1,
13631                                                anchor.coord.col() + 1,
13632                                            )
13633                                            .unwrap_or(LiteralValue::Empty);
13634                                        let new = LiteralValue::Error(e.clone());
13635                                        if old != new {
13636                                            d.record_cell(
13637                                                anchor.sheet_id,
13638                                                anchor.coord.row(),
13639                                                anchor.coord.col(),
13640                                            );
13641                                        }
13642                                    }
13643                                    let err_val = LiteralValue::Error(e.clone());
13644                                    self.graph.update_vertex_value_ref(vertex_id, &err_val);
13645                                    if self.config.arrow_storage_enabled
13646                                        && self.config.delta_overlay_enabled
13647                                        && self.config.write_formula_overlay_enabled
13648                                    {
13649                                        let sheet_name =
13650                                            self.graph.sheet_name(anchor.sheet_id).to_string();
13651                                        self.mirror_value_to_computed_overlay(
13652                                            &sheet_name,
13653                                            anchor.coord.row() + 1,
13654                                            anchor.coord.col() + 1,
13655                                            &err_val,
13656                                        );
13657                                    }
13658                                    return Ok(err_val);
13659                                }
13660                                // Anchor shows the top-left value, like Excel
13661                                let top_left = rows
13662                                    .first()
13663                                    .and_then(|r| r.first())
13664                                    .cloned()
13665                                    .unwrap_or(LiteralValue::Empty);
13666                                self.graph.update_vertex_value_ref(vertex_id, &top_left);
13667                                Ok(top_left)
13668                            }
13669                            Err(e) => {
13670                                self.clear_spill_projection_and_mirror(
13671                                    vertex_id,
13672                                    delta.as_deref_mut(),
13673                                );
13674                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
13675                                    .with_message(
13676                                        e.message.unwrap_or_else(|| "Spill blocked".to_string()),
13677                                    )
13678                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
13679                                        expected_rows: h,
13680                                        expected_cols: w,
13681                                    });
13682                                let spill_val = LiteralValue::Error(spill_err.clone());
13683                                if let Some(d) = delta.as_deref_mut() {
13684                                    let old = self
13685                                        .read_cell_value(
13686                                            self.graph.sheet_name(anchor.sheet_id),
13687                                            anchor.coord.row() + 1,
13688                                            anchor.coord.col() + 1,
13689                                        )
13690                                        .unwrap_or(LiteralValue::Empty);
13691                                    if old != spill_val {
13692                                        d.record_cell(
13693                                            anchor.sheet_id,
13694                                            anchor.coord.row(),
13695                                            anchor.coord.col(),
13696                                        );
13697                                    }
13698                                }
13699                                self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13700                                if self.config.arrow_storage_enabled
13701                                    && self.config.delta_overlay_enabled
13702                                    && self.config.write_formula_overlay_enabled
13703                                {
13704                                    let sheet_name =
13705                                        self.graph.sheet_name(anchor.sheet_id).to_string();
13706                                    self.mirror_value_to_computed_overlay(
13707                                        &sheet_name,
13708                                        anchor.coord.row() + 1,
13709                                        anchor.coord.col() + 1,
13710                                        &spill_val,
13711                                    );
13712                                }
13713                                Ok(spill_val)
13714                            }
13715                        }
13716                    }
13717                    other => {
13718                        // Scalar result: store value and ensure any previous spill is cleared
13719                        let spill_cells = self
13720                            .graph
13721                            .spill_cells_for_anchor(vertex_id)
13722                            .map(|cells| cells.to_vec())
13723                            .unwrap_or_default();
13724                        if let Some(d) = delta.as_deref_mut()
13725                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
13726                        {
13727                            if spill_cells.is_empty() {
13728                                let old = self
13729                                    .read_cell_value(
13730                                        self.graph.sheet_name(anchor.sheet_id),
13731                                        anchor.coord.row() + 1,
13732                                        anchor.coord.col() + 1,
13733                                    )
13734                                    .unwrap_or(LiteralValue::Empty);
13735                                if old != other {
13736                                    d.record_cell(
13737                                        anchor.sheet_id,
13738                                        anchor.coord.row(),
13739                                        anchor.coord.col(),
13740                                    );
13741                                }
13742                            } else {
13743                                for cell in spill_cells.iter() {
13744                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
13745                                    let old = self
13746                                        .get_cell_value(
13747                                            sheet_name,
13748                                            cell.coord.row() + 1,
13749                                            cell.coord.col() + 1,
13750                                        )
13751                                        .unwrap_or(LiteralValue::Empty);
13752                                    let new = if cell.sheet_id == anchor.sheet_id
13753                                        && cell.coord.row() == anchor.coord.row()
13754                                        && cell.coord.col() == anchor.coord.col()
13755                                    {
13756                                        other.clone()
13757                                    } else {
13758                                        LiteralValue::Empty
13759                                    };
13760                                    Self::record_cell_if_changed(d, cell, &old, &new);
13761                                }
13762                            }
13763                        }
13764                        self.graph.clear_spill_region(vertex_id);
13765                        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
13766                            self.record_structural_change(scope);
13767                        }
13768                        if self.config.arrow_storage_enabled
13769                            && self.config.delta_overlay_enabled
13770                            && self.config.write_formula_overlay_enabled
13771                        {
13772                            let empty = LiteralValue::Empty;
13773                            for cell in spill_cells.iter() {
13774                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
13775                                self.mirror_value_to_computed_overlay(
13776                                    &sheet_name,
13777                                    cell.coord.row() + 1,
13778                                    cell.coord.col() + 1,
13779                                    &empty,
13780                                );
13781                            }
13782                        }
13783                        self.graph.update_vertex_value_ref(vertex_id, &other);
13784                        // Optionally mirror into Arrow overlay for Arrow-backed reads
13785                        if self.config.arrow_storage_enabled
13786                            && self.config.delta_overlay_enabled
13787                            && self.config.write_formula_overlay_enabled
13788                        {
13789                            let anchor = self
13790                                .graph
13791                                .get_cell_ref(vertex_id)
13792                                .expect("cell ref for vertex");
13793                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13794                            self.mirror_value_to_computed_overlay(
13795                                &sheet_name,
13796                                anchor.coord.row() + 1,
13797                                anchor.coord.col() + 1,
13798                                &other,
13799                            );
13800                        }
13801                        Ok(other)
13802                    }
13803                }
13804            }
13805            Err(e) => {
13806                // Runtime Excel error: store as a cell value instead of propagating
13807                // as an exception so bulk eval paths don't fail the whole pass.
13808                let spill_cells = self
13809                    .graph
13810                    .spill_cells_for_anchor(vertex_id)
13811                    .map(|cells| cells.to_vec())
13812                    .unwrap_or_default();
13813                let err_val = LiteralValue::Error(e.clone());
13814                if let Some(d) = delta
13815                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
13816                {
13817                    if spill_cells.is_empty() {
13818                        let old = self
13819                            .read_cell_value(
13820                                self.graph.sheet_name(anchor.sheet_id),
13821                                anchor.coord.row() + 1,
13822                                anchor.coord.col() + 1,
13823                            )
13824                            .unwrap_or(LiteralValue::Empty);
13825                        if old != err_val {
13826                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
13827                        }
13828                    } else {
13829                        for cell in spill_cells.iter() {
13830                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
13831                            let old = self
13832                                .get_cell_value(
13833                                    sheet_name,
13834                                    cell.coord.row() + 1,
13835                                    cell.coord.col() + 1,
13836                                )
13837                                .unwrap_or(LiteralValue::Empty);
13838                            let new = if cell.sheet_id == anchor.sheet_id
13839                                && cell.coord.row() == anchor.coord.row()
13840                                && cell.coord.col() == anchor.coord.col()
13841                            {
13842                                err_val.clone()
13843                            } else {
13844                                LiteralValue::Empty
13845                            };
13846                            Self::record_cell_if_changed(d, cell, &old, &new);
13847                        }
13848                    }
13849                }
13850                self.graph.clear_spill_region(vertex_id);
13851                if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
13852                    self.record_structural_change(scope);
13853                }
13854                if self.config.arrow_storage_enabled
13855                    && self.config.delta_overlay_enabled
13856                    && self.config.write_formula_overlay_enabled
13857                {
13858                    let empty = LiteralValue::Empty;
13859                    for cell in spill_cells.iter() {
13860                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
13861                        self.mirror_value_to_computed_overlay(
13862                            &sheet_name,
13863                            cell.coord.row() + 1,
13864                            cell.coord.col() + 1,
13865                            &empty,
13866                        );
13867                    }
13868                }
13869                self.graph.update_vertex_value_ref(vertex_id, &err_val);
13870                if self.config.arrow_storage_enabled
13871                    && self.config.delta_overlay_enabled
13872                    && self.config.write_formula_overlay_enabled
13873                {
13874                    let anchor = self
13875                        .graph
13876                        .get_cell_ref(vertex_id)
13877                        .expect("cell ref for vertex");
13878                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13879                    self.mirror_value_to_computed_overlay(
13880                        &sheet_name,
13881                        anchor.coord.row() + 1,
13882                        anchor.coord.col() + 1,
13883                        &err_val,
13884                    );
13885                }
13886                Ok(err_val)
13887            }
13888        }
13889    }
13890
13891    fn evaluate_named_scalar(
13892        &mut self,
13893        vertex_id: VertexId,
13894        sheet_id: SheetId,
13895    ) -> Result<LiteralValue, ExcelError> {
13896        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
13897            ExcelError::new(ExcelErrorKind::Name)
13898                .with_message("Named range metadata missing".to_string())
13899        })?;
13900
13901        match &named_range.definition {
13902            NamedDefinition::Cell(cell_ref) => {
13903                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13904                let row = cell_ref.coord.row() + 1;
13905                let col = cell_ref.coord.col() + 1;
13906
13907                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
13908                    && matches!(
13909                        self.graph.get_vertex_kind(dep_vertex),
13910                        VertexKind::FormulaScalar | VertexKind::FormulaArray
13911                    )
13912                {
13913                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
13914                    let value = self.evaluate_vertex(dep_vertex)?;
13915                    self.graph.update_vertex_value_ref(vertex_id, &value);
13916                    Ok(value)
13917                } else {
13918                    let value = self
13919                        .get_cell_value(sheet_name, row, col)
13920                        .unwrap_or(LiteralValue::Empty);
13921                    self.graph.update_vertex_value_ref(vertex_id, &value);
13922                    Ok(value)
13923                }
13924            }
13925            NamedDefinition::Literal(v) => {
13926                let out = v.clone();
13927                self.graph.update_vertex_value_ref(vertex_id, &out);
13928                Ok(out)
13929            }
13930            NamedDefinition::Formula { ast, .. } => {
13931                let context_sheet = match named_range.scope {
13932                    NameScope::Sheet(id) => id,
13933                    NameScope::Workbook => sheet_id,
13934                };
13935                let sheet_name = self.graph.sheet_name(context_sheet);
13936                let cell_ref = self
13937                    .graph
13938                    .get_cell_ref(vertex_id)
13939                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
13940                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13941                match interpreter.evaluate_ast(ast) {
13942                    Ok(cv) => {
13943                        let value = cv.into_literal();
13944                        match value {
13945                            LiteralValue::Array(_) => {
13946                                let err = ExcelError::new(ExcelErrorKind::NImpl)
13947                                    .with_message("Array result in scalar named range".to_string());
13948                                let err_val = LiteralValue::Error(err.clone());
13949                                self.graph.update_vertex_value_ref(vertex_id, &err_val);
13950                                Ok(err_val)
13951                            }
13952                            other => {
13953                                self.graph.update_vertex_value_ref(vertex_id, &other);
13954                                Ok(other)
13955                            }
13956                        }
13957                    }
13958                    Err(err) => {
13959                        let err_val = LiteralValue::Error(err.clone());
13960                        self.graph.update_vertex_value_ref(vertex_id, &err_val);
13961                        Ok(err_val)
13962                    }
13963                }
13964            }
13965            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
13966                .with_message("Range-valued name evaluated as scalar".to_string())),
13967        }
13968    }
13969
13970    fn evaluate_named_array(
13971        &mut self,
13972        vertex_id: VertexId,
13973        sheet_id: SheetId,
13974    ) -> Result<LiteralValue, ExcelError> {
13975        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
13976            ExcelError::new(ExcelErrorKind::Name)
13977                .with_message("Named range metadata missing".to_string())
13978        })?;
13979
13980        let out = match &named_range.definition {
13981            NamedDefinition::Range(range_ref) => {
13982                if range_ref.start.sheet_id != range_ref.end.sheet_id {
13983                    return Err(ExcelError::new(ExcelErrorKind::Ref)
13984                        .with_message("Named range cannot span sheets".to_string()));
13985                }
13986
13987                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
13988                let sr0 = range_ref.start.coord.row();
13989                let sc0 = range_ref.start.coord.col();
13990                let er0 = range_ref.end.coord.row();
13991                let ec0 = range_ref.end.coord.col();
13992                if sr0 > er0 || sc0 > ec0 {
13993                    return Err(ExcelError::new(ExcelErrorKind::Ref)
13994                        .with_message("Invalid named range bounds".to_string()));
13995                }
13996
13997                let h = (er0 - sr0 + 1) as usize;
13998                let w = (ec0 - sc0 + 1) as usize;
13999                let cell_count = (h as u64).saturating_mul(w as u64);
14000                if cell_count > self.config.spill.max_spill_cells as u64 {
14001                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
14002                        "Named range too large to materialize as an array".to_string(),
14003                    ));
14004                }
14005
14006                let mut rows = Vec::with_capacity(h);
14007                for r0 in sr0..=er0 {
14008                    let mut row = Vec::with_capacity(w);
14009                    for c0 in sc0..=ec0 {
14010                        let v = self
14011                            .get_cell_value(sheet_name, r0 + 1, c0 + 1)
14012                            .unwrap_or(LiteralValue::Empty);
14013                        row.push(v);
14014                    }
14015                    rows.push(row);
14016                }
14017                LiteralValue::Array(rows)
14018            }
14019            NamedDefinition::Cell(cell_ref) => {
14020                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
14021                let row = cell_ref.coord.row() + 1;
14022                let col = cell_ref.coord.col() + 1;
14023                let v = self
14024                    .get_cell_value(sheet_name, row, col)
14025                    .unwrap_or(LiteralValue::Empty);
14026                LiteralValue::Array(vec![vec![v]])
14027            }
14028            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
14029            NamedDefinition::Formula { ast, .. } => {
14030                let context_sheet = match named_range.scope {
14031                    NameScope::Sheet(id) => id,
14032                    NameScope::Workbook => sheet_id,
14033                };
14034                let sheet_name = self.graph.sheet_name(context_sheet);
14035                let cell_ref = self
14036                    .graph
14037                    .get_cell_ref(vertex_id)
14038                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
14039                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
14040                match interpreter.evaluate_ast(ast) {
14041                    Ok(cv) => {
14042                        let v = cv.into_literal();
14043                        match v {
14044                            LiteralValue::Array(_) => v,
14045                            other => LiteralValue::Array(vec![vec![other]]),
14046                        }
14047                    }
14048                    Err(err) => LiteralValue::Error(err),
14049                }
14050            }
14051        };
14052
14053        self.graph.update_vertex_value_ref(vertex_id, &out);
14054        Ok(out)
14055    }
14056
14057    fn replan_exhausted_error(&self, limit: usize, context: &str) -> ExcelError {
14058        crate::engine::ResourceLedgerError::Exhausted(
14059            formualizer_common::ResourceExhaustionDetail {
14060                reason: formualizer_common::ResourceExhaustionReason::WorkUnits,
14061                limit: limit as u64,
14062                observed: limit.saturating_add(1) as u64,
14063                request_id: self
14064                    .active_evaluation_resource_request
14065                    .as_ref()
14066                    .map(|stats| stats.request_id),
14067            },
14068        )
14069        .into_excel_error()
14070        .with_message(format!("{context} did not converge after {limit} replans"))
14071    }
14072
14073    fn transient_target_preparation_stale(error: &ExcelError) -> bool {
14074        matches!(
14075            &error.extra,
14076            formualizer_common::ExcelErrorExtra::PreparationStale {
14077                reason: formualizer_common::PreparationStaleReason::Semantic
14078                    | formualizer_common::PreparationStaleReason::Provider
14079            }
14080        )
14081    }
14082
14083    fn prepare_graph_for_routed_evaluation(
14084        &mut self,
14085        targets: &[crate::engine::EvaluationTarget],
14086        options: &crate::engine::TargetEvalOptions<'_>,
14087    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
14088        const MAX_TRANSIENT_PREPARATION_RETRIES: usize = 2;
14089        let mut retries = 0usize;
14090        loop {
14091            match self.prepare_graph_for_targets_unobserved(targets, options) {
14092                Err(error)
14093                    if Self::transient_target_preparation_stale(&error)
14094                        && retries < MAX_TRANSIENT_PREPARATION_RETRIES =>
14095                {
14096                    retries = retries.saturating_add(1);
14097                }
14098                result => return result,
14099            }
14100        }
14101    }
14102
14103    fn evaluate_mixed_targets(
14104        &mut self,
14105        targets: &[crate::engine::EvaluationTarget],
14106        delta: Option<&mut DeltaCollector>,
14107    ) -> Result<EvalResult, ExcelError> {
14108        let _source_cache = self.source_cache_session();
14109        let cancel = self.active_cancel_flag.clone();
14110        let options = crate::engine::TargetEvalOptions {
14111            request_id: self
14112                .active_evaluation_resource_request
14113                .as_ref()
14114                .map(|stats| stats.request_id),
14115            cancel,
14116            deadline: None,
14117            budgets: None,
14118            opaque_policy: crate::engine::OpaquePreparePolicy::Widen,
14119        };
14120        self.prepare_and_execute_target_recipe(targets, &options, delta)
14121    }
14122
14123    fn prepare_and_execute_target_recipe(
14124        &mut self,
14125        targets: &[crate::engine::EvaluationTarget],
14126        options: &crate::engine::TargetEvalOptions<'_>,
14127        delta: Option<&mut DeltaCollector>,
14128    ) -> Result<EvalResult, ExcelError> {
14129        let preparation = self.prepare_graph_for_routed_evaluation(targets, options)?;
14130        self.execute_prepared_target_recipe(targets, &preparation.widened_scope, delta)
14131    }
14132
14133    fn execute_prepared_target_recipe(
14134        &mut self,
14135        targets: &[crate::engine::EvaluationTarget],
14136        scope: &crate::engine::PrepareScope,
14137        delta: Option<&mut DeltaCollector>,
14138    ) -> Result<EvalResult, ExcelError> {
14139        self.require_unified_authority()?;
14140        if matches!(scope, crate::engine::PrepareScope::Workbook)
14141            && let Some(stats) = self.active_evaluation_resource_request.as_mut()
14142        {
14143            stats.workbook_exact_attempts = stats.workbook_exact_attempts.max(1);
14144        }
14145        let mut roots = self.resolve_target_producers(targets)?;
14146        if let crate::engine::PrepareScope::Sheets(sheets) = scope {
14147            let request_id = self
14148                .active_evaluation_resource_request
14149                .as_ref()
14150                .map(|request| request.request_id);
14151            let root_count = roots.len();
14152            let mut widened_roots =
14153                OrderedTargetProducers::from_ordered(std::mem::take(&mut roots))
14154                    .map_err(|_| target_root_allocation_error(root_count, request_id))?;
14155            let sheet_ids = sheets
14156                .iter()
14157                .filter_map(|sheet| self.graph.sheet_id(sheet))
14158                .collect::<FxHashSet<_>>();
14159            for vertex in self.graph.formula_vertices() {
14160                if sheet_ids.contains(&self.graph.get_vertex_sheet_id(vertex)) {
14161                    widened_roots
14162                        .push(crate::engine::target_preparation::TargetProducer::Legacy(
14163                            vertex,
14164                        ))
14165                        .map_err(|_| {
14166                            target_root_allocation_error(widened_roots.len() + 1, request_id)
14167                        })?;
14168                }
14169            }
14170            roots = widened_roots.into_vec();
14171        }
14172        self.begin_evaluation_request();
14173        #[cfg(any(test, feature = "legacy_oracle"))]
14174        self.graph.flush_pending_edge_deltas();
14175        let workbook_scope = matches!(scope, crate::engine::PrepareScope::Workbook);
14176        if workbook_scope {
14177            if let Some(delta) = delta {
14178                self.evaluate_all_with_delta_collector(delta)
14179            } else {
14180                self.evaluate_all_legacy_impl()
14181            }
14182        } else {
14183            self.evaluate_legacy_target_roots(&roots, delta)
14184        }
14185    }
14186
14187    fn legacy_coordinate_targets(
14188        &mut self,
14189        targets: &[(&str, u32, u32)],
14190    ) -> Vec<crate::engine::EvaluationTarget> {
14191        targets
14192            .iter()
14193            .map(|(sheet, row, col)| {
14194                // Compatibility APIs historically interned an unknown target sheet
14195                // and returned an empty value rather than rejecting the target.
14196                self.graph.sheet_id_mut(sheet);
14197                crate::engine::EvaluationTarget::Cell {
14198                    sheet: (*sheet).to_string(),
14199                    row: *row,
14200                    col: *col,
14201                }
14202            })
14203            .collect()
14204    }
14205
14206    /// Evaluate the necessary mixed producer closure for typed cell, range, name, and table targets.
14207    pub fn evaluate_targets(
14208        &mut self,
14209        targets: &[crate::engine::EvaluationTarget],
14210    ) -> Result<EvalResult, ExcelError> {
14211        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14212            engine.observe_function_semantic_epoch()?;
14213            engine.validate_deterministic_mode()?;
14214            engine.evaluate_mixed_targets(targets, None)
14215        })
14216    }
14217
14218    /// Evaluate typed targets with explicit preparation policy and request controls.
14219    pub fn evaluate_targets_with_options(
14220        &mut self,
14221        targets: &[crate::engine::EvaluationTarget],
14222        options: crate::engine::TargetEvalOptions<'_>,
14223    ) -> Result<EvalResult, ExcelError> {
14224        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14225            engine.active_cancel_flag = options.cancel.clone();
14226            engine.active_evaluation_deadline = options.deadline;
14227            let result = (|| {
14228                engine.cancellation_checkpoint("Evaluation cancelled before target preparation")?;
14229                engine.observe_function_semantic_epoch()?;
14230                engine.validate_deterministic_mode()?;
14231                let _source_cache = engine.source_cache_session();
14232                engine.prepare_and_execute_target_recipe(targets, &options, None)
14233            })();
14234            engine.active_cancel_flag = None;
14235            engine.active_evaluation_deadline = None;
14236            result
14237        })
14238    }
14239
14240    /// Evaluate typed targets and return the versioned run/region delta for the request.
14241    pub fn evaluate_targets_with_delta(
14242        &mut self,
14243        targets: &[crate::engine::EvaluationTarget],
14244    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
14245        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
14246            engine.observe_function_semantic_epoch()?;
14247            engine.validate_deterministic_mode()?;
14248            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14249            let result = engine.evaluate_mixed_targets(targets, Some(&mut collector))?;
14250            Ok((result, collector.finish_target()))
14251        })
14252    }
14253
14254    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
14255    pub fn evaluate_until(
14256        &mut self,
14257        targets: &[(&str, u32, u32)],
14258    ) -> Result<EvalResult, ExcelError> {
14259        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14260            engine.evaluate_until_unobserved(targets)
14261        })
14262    }
14263
14264    fn evaluate_until_unobserved(
14265        &mut self,
14266        targets: &[(&str, u32, u32)],
14267    ) -> Result<EvalResult, ExcelError> {
14268        self.observe_function_semantic_epoch()?;
14269        let targets = self.legacy_coordinate_targets(targets);
14270        self.evaluate_mixed_targets(&targets, None)
14271    }
14272
14273    fn evaluate_until_with_delta_collector(
14274        &mut self,
14275        targets: &[(&str, u32, u32)],
14276        delta: &mut DeltaCollector,
14277    ) -> Result<EvalResult, ExcelError> {
14278        let targets = self.legacy_coordinate_targets(targets);
14279        self.evaluate_mixed_targets(&targets, Some(delta))
14280    }
14281
14282    fn evaluate_legacy_target_roots(
14283        &mut self,
14284        roots: &[crate::engine::target_preparation::TargetProducer],
14285        mut delta: Option<&mut DeltaCollector>,
14286    ) -> Result<EvalResult, ExcelError> {
14287        use crate::engine::target_preparation::TargetProducer;
14288        #[cfg(any(test, feature = "benchmark_internal"))]
14289        {
14290            self.recalc_reuse_probe
14291                .get_mut()
14292                .unwrap()
14293                .legacy_target_requests += 1;
14294        }
14295        let start = crate::instant::FzInstant::now();
14296        let root_vertices = roots
14297            .iter()
14298            .filter_map(|root| match root {
14299                TargetProducer::Legacy(vertex) | TargetProducer::Symbol(vertex) => Some(*vertex),
14300                TargetProducer::ValueOnly(_) => None,
14301            })
14302            .collect::<Vec<_>>();
14303        let mut computed_vertices = 0usize;
14304        let mut cycle_errors = 0usize;
14305        let mut replans = 0usize;
14306        const MAX_REPLAN: usize = 5;
14307        self.graph.authority_sync();
14308        loop {
14309            let (precedents_to_eval, old_vdeps) = self.demand_subgraph(&root_vertices)?;
14310            if precedents_to_eval.is_empty() {
14311                break;
14312            }
14313            #[cfg(any(test, feature = "benchmark_internal"))]
14314            {
14315                self.recalc_reuse_probe
14316                    .get_mut()
14317                    .unwrap()
14318                    .target_schedule_builds += 1;
14319            }
14320            let schedule = {
14321                self.graph.authority_sync();
14322                let mut ledger = self.active_resource_ledger.take();
14323                let result = self.create_authority_schedule(
14324                    &precedents_to_eval,
14325                    &old_vdeps,
14326                    ledger.as_mut(),
14327                );
14328                self.active_resource_ledger = ledger;
14329                result?
14330            };
14331            self.begin_pass(&schedule);
14332            for (unit_index, &unit) in schedule.units.iter().enumerate() {
14333                self.cancellation_checkpoint("Evaluation cancelled before target schedule unit")?;
14334                match unit {
14335                    ScheduleUnit::Cycle(index) => {
14336                        if self.handle_cycle_unit(
14337                            schedule.unit_cycle(index),
14338                            delta.as_deref_mut(),
14339                            None,
14340                            None,
14341                        )? > 0
14342                        {
14343                            cycle_errors = cycle_errors.saturating_add(1);
14344                        }
14345                    }
14346                    ScheduleUnit::Layer(index) => {
14347                        let layer = schedule.unit_layer(index);
14348                        let evaluated = if let Some(delta) = delta.as_deref_mut() {
14349                            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14350                                self.evaluate_layer_parallel_with_delta(layer, delta)?
14351                            } else {
14352                                self.evaluate_layer_sequential_with_delta(layer, delta)?
14353                            }
14354                        } else if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14355                            self.evaluate_layer_parallel(layer)?
14356                        } else {
14357                            self.evaluate_layer_sequential(layer)?
14358                        };
14359                        computed_vertices = computed_vertices.saturating_add(evaluated);
14360                    }
14361                }
14362                if self.stop_after_unit(&schedule, unit_index) {
14363                    break;
14364                }
14365            }
14366            let changed = self.changed_virtual_dep_vertices(&precedents_to_eval, &old_vdeps);
14367            self.resource_checkpoint(0)?;
14368            if !self.finish_target_pass_dirty(&precedents_to_eval, &changed) {
14369                break;
14370            }
14371            if replans >= MAX_REPLAN {
14372                return Err(self.replan_exhausted_error(
14373                    MAX_REPLAN,
14374                    "targeted legacy dynamic dependency evaluation",
14375                ));
14376            }
14377            replans = replans.saturating_add(1);
14378        }
14379        self.redirty_for_next_recalc();
14380        Ok(EvalResult {
14381            computed_vertices,
14382            cycle_errors,
14383            elapsed: start.elapsed(),
14384        })
14385    }
14386
14387    /// Build a revision-bound compatibility plan covering every prepared formula vertex.
14388    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
14389        if self.has_staged_formulas() || self.staged_formula_index.has_packages() {
14390            return Err(
14391                Self::plan_stale(formualizer_common::PlanStaleReason::Staged).with_message(
14392                    "compatibility recalculation plans require all staged formulas to be prepared",
14393                ),
14394            );
14395        }
14396        let key = self.recalc_plan_key();
14397        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
14398        vertices.sort_unstable();
14399        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
14400        let schedule = if vertices.is_empty() {
14401            crate::engine::Schedule {
14402                units: Vec::new(),
14403                layers: Vec::new(),
14404                cycles: Vec::new(),
14405            }
14406        } else {
14407            self.create_evaluation_schedule_uncached(&vertices, None)?.0
14408        };
14409        self.validate_recalc_plan_key(&key)?;
14410        Ok(RecalcPlan {
14411            key,
14412            kind: RecalcPlanKind::CompatibilityFull {
14413                schedule,
14414                has_dynamic_refs,
14415            },
14416        })
14417    }
14418
14419    /// Prepare stable typed targets and retain a revision-bound run-local recipe.
14420    pub fn build_recalc_plan_for_targets(
14421        &mut self,
14422        targets: &[crate::engine::EvaluationTarget],
14423    ) -> Result<RecalcPlan, ExcelError> {
14424        self.build_recalc_plan_for_targets_with_options(
14425            targets,
14426            crate::engine::TargetEvalOptions::default(),
14427        )
14428    }
14429
14430    pub fn build_recalc_plan_for_targets_with_options(
14431        &mut self,
14432        targets: &[crate::engine::EvaluationTarget],
14433        options: crate::engine::TargetEvalOptions<'_>,
14434    ) -> Result<RecalcPlan, ExcelError> {
14435        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14436            engine.observe_function_semantic_epoch()?;
14437            engine.validate_deterministic_mode()?;
14438            let _source_cache = engine.source_cache_session();
14439            let preparation = engine.prepare_graph_for_routed_evaluation(targets, &options)?;
14440            #[cfg(any(test, feature = "legacy_oracle"))]
14441            engine.graph.flush_pending_edge_deltas();
14442            let topology = if matches!(
14443                preparation.widened_scope,
14444                crate::engine::PrepareScope::Workbook
14445            ) {
14446                RecalcTopology::Workbook
14447            } else {
14448                RecalcTopology::RunLocalRecipe
14449            };
14450            Ok(RecalcPlan {
14451                key: engine.recalc_plan_key(),
14452                kind: RecalcPlanKind::Target {
14453                    targets: targets.to_vec(),
14454                    scope: preparation.widened_scope,
14455                    topology,
14456                    dynamic_policy: DynamicPlanPolicy::BoundedTargetReplan,
14457                },
14458            })
14459        })
14460    }
14461
14462    /// Evaluate using a previously constructed compatibility or target plan.
14463    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
14464        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14465            engine.evaluate_recalc_plan_unobserved(plan)
14466        })
14467    }
14468
14469    pub fn evaluate_recalc_plan_with_controls(
14470        &mut self,
14471        plan: &RecalcPlan,
14472        cancel: Option<crate::engine::CancelToken>,
14473        deadline: Option<Instant>,
14474    ) -> Result<EvalResult, ExcelError> {
14475        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14476            engine.active_cancel_flag = cancel.clone();
14477            engine.active_evaluation_deadline = deadline;
14478            let result = engine.evaluate_recalc_plan_unobserved(plan);
14479            engine.active_cancel_flag = None;
14480            engine.active_evaluation_deadline = None;
14481            result
14482        })
14483    }
14484
14485    fn evaluate_recalc_plan_unobserved(
14486        &mut self,
14487        plan: &RecalcPlan,
14488    ) -> Result<EvalResult, ExcelError> {
14489        #[cfg(any(test, feature = "legacy_oracle"))]
14490        self.graph.flush_pending_edge_deltas();
14491        self.validate_recalc_plan_key(&plan.key)?;
14492        self.cancellation_checkpoint("Evaluation cancelled before recalculation plan execution")?;
14493        self.validate_deterministic_mode()?;
14494
14495        match &plan.kind {
14496            RecalcPlanKind::Target {
14497                targets,
14498                scope,
14499                topology,
14500                dynamic_policy,
14501            } => {
14502                debug_assert_eq!(*dynamic_policy, DynamicPlanPolicy::BoundedTargetReplan);
14503                debug_assert_eq!(
14504                    matches!(topology, RecalcTopology::Workbook),
14505                    matches!(scope, crate::engine::PrepareScope::Workbook)
14506                );
14507                let _source_cache = self.source_cache_session();
14508                self.execute_prepared_target_recipe(targets, scope, None)
14509            }
14510            RecalcPlanKind::CompatibilityFull {
14511                schedule,
14512                has_dynamic_refs,
14513            } => {
14514                let _source_cache = self.source_cache_session();
14515                self.begin_evaluation_request();
14516                if *has_dynamic_refs {
14517                    self.virtual_dep_fallback_activations =
14518                        self.virtual_dep_fallback_activations.saturating_add(1);
14519                    return self.evaluate_all_coordinator();
14520                }
14521
14522                let start = crate::instant::FzInstant::now();
14523                let dirty_vertices = self.graph.get_evaluation_vertices();
14524                if dirty_vertices.is_empty() {
14525                    return Ok(EvalResult {
14526                        computed_vertices: 0,
14527                        cycle_errors: 0,
14528                        elapsed: start.elapsed(),
14529                    });
14530                }
14531
14532                let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
14533                let mut computed_vertices = 0;
14534                let mut cycle_errors = 0;
14535                for &unit in &schedule.units {
14536                    self.cancellation_checkpoint(
14537                        "Evaluation cancelled before recalculation plan schedule unit",
14538                    )?;
14539                    match unit {
14540                        ScheduleUnit::Cycle(i) => {
14541                            let stamped = self.handle_cycle_unit(
14542                                schedule.unit_cycle(i),
14543                                None,
14544                                Some(&dirty_set),
14545                                None,
14546                            )?;
14547                            if stamped > 0 {
14548                                cycle_errors += 1;
14549                            }
14550                        }
14551                        ScheduleUnit::Layer(i) => {
14552                            let work: Vec<VertexId> = schedule
14553                                .unit_layer(i)
14554                                .vertices
14555                                .iter()
14556                                .copied()
14557                                .filter(|v| dirty_set.contains(v))
14558                                .collect();
14559                            if work.is_empty() {
14560                                continue;
14561                            }
14562                            let temp_layer = crate::engine::scheduler::Layer::new(work);
14563                            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
14564                                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
14565                            } else {
14566                                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
14567                            }
14568                        }
14569                    }
14570                }
14571
14572                self.resource_checkpoint(0)?;
14573                self.graph.clear_dirty_flags(&dirty_vertices);
14574                self.redirty_for_next_recalc();
14575                Ok(EvalResult {
14576                    computed_vertices,
14577                    cycle_errors,
14578                    elapsed: start.elapsed(),
14579                })
14580            }
14581        }
14582    }
14583}
14584
14585impl<R> Engine<R>
14586where
14587    R: EvaluationContext,
14588{
14589    /// Refuse out-of-scope authority states before evaluation can demote spans
14590    /// or execute a legacy schedule. The public error type is unchanged; NImpl
14591    /// carries the exact internal Unsupported operation for the deferred-scope
14592    /// gate. Admission and allocation failures are not scope exceptions.
14593    fn require_unified_authority(&mut self) -> Result<(), ExcelError> {
14594        self.graph
14595            .authority()
14596            .map(|_| ())
14597            .map_err(Self::authority_excel_error)
14598    }
14599
14600    fn authority_excel_error(error: crate::engine::authority::store::AuthorityError) -> ExcelError {
14601        use crate::engine::authority::store::AuthorityError;
14602        // Some unchanged behavioral tests assert only `error.kind`, hiding
14603        // the operation in their panic. The opt-in gate trace proves which
14604        // typed error was actually returned; it never changes that error.
14605        #[cfg(test)]
14606        if std::env::var_os("FZ_AUTHORITY_DEFERRED_TRACE").is_some() {
14607            eprintln!("M1B_AUTHORITY_ERROR {error:?}");
14608        }
14609        let kind = match error {
14610            AuthorityError::Unsupported { .. } => ExcelErrorKind::NImpl,
14611            _ => ExcelErrorKind::Error,
14612        };
14613        ExcelError::new(kind).with_message(format!("unified_authority: {error:?}"))
14614    }
14615
14616    /// Evaluate all dirty/volatile vertices
14617    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
14618        // `evaluate_all_unobserved` owns the `observe_function_semantic_epoch` guard.
14619        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
14620            engine.evaluate_all_unobserved()
14621        })
14622    }
14623
14624    fn evaluate_all_unobserved(&mut self) -> Result<EvalResult, ExcelError> {
14625        debug_assert!(
14626            !self.graph.deferred_dirty_active(),
14627            "deferred-dirty scope leaked into evaluate_all: a begin_deferred_dirty \
14628             was not balanced by end_deferred_dirty"
14629        );
14630        self.observe_function_semantic_epoch()?;
14631        self.lookup_index_cache.reset_counters();
14632        let _source_cache = self.source_cache_session();
14633        self.validate_deterministic_mode()?;
14634        if self.config.defer_graph_building {
14635            // Build graph for all staged formulas before evaluating
14636            self.build_graph_all()?;
14637        }
14638        self.evaluate_all_coordinator()
14639    }
14640
14641    /// Coordinator for `evaluate_all`: starts the evaluation request and runs
14642    /// the per-cell pass.
14643    fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
14644        self.require_unified_authority()?;
14645        self.begin_evaluation_request();
14646        self.evaluate_all_legacy_impl()
14647    }
14648
14649    /// Walk a schedule's units in condensation order: stamp each cyclic SCC
14650    /// at its position and evaluate each layer (parallel when enabled).
14651    ///
14652    /// Returns `(computed_vertices, cycle_count)` where `cycle_count` is the
14653    /// number of Cycle units walked (the former `schedule.cycles.len()`).
14654    fn legacy_pass_run_units(
14655        &mut self,
14656        schedule: &crate::engine::scheduler::Schedule,
14657    ) -> Result<(usize, usize), ExcelError> {
14658        let mut computed_vertices = 0;
14659        let mut cycle_count = 0;
14660        self.begin_pass(schedule);
14661        for (unit_index, &unit) in schedule.units.iter().enumerate() {
14662            match unit {
14663                ScheduleUnit::Cycle(i) => {
14664                    if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0 {
14665                        cycle_count += 1;
14666                    }
14667                }
14668                ScheduleUnit::Layer(i) => {
14669                    let layer = schedule.unit_layer(i);
14670                    if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14671                        computed_vertices += self.evaluate_layer_parallel(layer)?;
14672                    } else {
14673                        computed_vertices += self.evaluate_layer_sequential(layer)?;
14674                    }
14675                }
14676            }
14677            if self.stop_after_unit(schedule, unit_index) {
14678                break;
14679            }
14680        }
14681        Ok((computed_vertices, cycle_count))
14682    }
14683
14684    /// Per-cell `evaluate_all` body, reached through the coordinator. This is
14685    /// an internal primitive; it must not be invoked directly from public APIs.
14686    ///
14687    /// Does NOT call `begin_evaluation_request` (cycle-telemetry reset +
14688    /// per-recalc clock sample): request begin happens at the public entry
14689    /// points / coordinators, so one request keeps one clock sample.
14690    fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
14691        self.reset_virtual_dep_telemetry_if_disabled();
14692        let _span_eval =
14693            crate::engine::trace::fz_span!(tracing::Level::INFO, "evaluate", "evaluate.legacy");
14694        let start = crate::instant::FzInstant::now();
14695        let mut computed_vertices = 0;
14696        let mut cycle_errors = 0;
14697        let mut replan_iterations = 0;
14698        const MAX_REPLAN: usize = 5;
14699        let mut telemetry = self
14700            .config
14701            .enable_virtual_dep_telemetry
14702            .then(|| self.start_virtual_dep_telemetry());
14703
14704        loop {
14705            let to_evaluate = self.graph.get_evaluation_vertices();
14706            if to_evaluate.is_empty() {
14707                if let Some(t) = telemetry.as_mut()
14708                    && t.bailout_reason.is_none()
14709                {
14710                    t.bailout_reason = Some("no_work");
14711                }
14712                break;
14713            }
14714
14715            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
14716            if let Some(t) = telemetry.as_mut() {
14717                Self::accumulate_schedule_meta(t, &meta);
14718            }
14719
14720            let (pass_computed, pass_cycles) = self.legacy_pass_run_units(&schedule)?;
14721            computed_vertices += pass_computed;
14722            cycle_errors += pass_cycles;
14723
14724            // Check if dynamic dependencies changed
14725            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
14726            if let Some(t) = telemetry.as_mut() {
14727                t.changed_vdeps_total += changed_vertices.len();
14728            }
14729
14730            self.resource_checkpoint(0)?;
14731            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
14732                if let Some(t) = telemetry.as_mut() {
14733                    t.bailout_reason = Some("converged");
14734                }
14735                break;
14736            }
14737            if replan_iterations >= MAX_REPLAN {
14738                if let Some(mut t) = telemetry.take() {
14739                    t.bailout_reason = Some("max_replan");
14740                    t.replan_iterations = replan_iterations;
14741                    self.last_virtual_dep_telemetry = t;
14742                }
14743                return Err(
14744                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
14745                );
14746            }
14747
14748            replan_iterations += 1;
14749        }
14750
14751        if let Some(mut t) = telemetry {
14752            t.replan_iterations = replan_iterations;
14753            self.last_virtual_dep_telemetry = t;
14754        }
14755
14756        // Re-dirty volatile vertices for the next evaluation cycle
14757        self.redirty_for_next_recalc();
14758
14759        // Advance recalc epoch after a full evaluation pass finishes
14760        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
14761
14762        Ok(EvalResult {
14763            computed_vertices,
14764            cycle_errors,
14765            elapsed: start.elapsed(),
14766        })
14767    }
14768
14769    pub fn evaluate_all_with_target_delta(
14770        &mut self,
14771    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
14772        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
14773            engine.observe_function_semantic_epoch()?;
14774            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14775            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
14776            Ok((result, collector.finish_target()))
14777        })
14778    }
14779
14780    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
14781        self.evaluate_all_with_delta_policy(EvalDeltaCompatibilityPolicy::Unlimited)
14782    }
14783
14784    pub fn evaluate_all_with_delta_policy(
14785        &mut self,
14786        policy: EvalDeltaCompatibilityPolicy,
14787    ) -> Result<(EvalResult, EvalDelta), ExcelError> {
14788        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
14789            engine.observe_function_semantic_epoch()?;
14790            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14791            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
14792            Ok((result, collector.finish_with_policy(policy)?))
14793        })
14794    }
14795
14796    fn evaluate_all_with_delta_collector(
14797        &mut self,
14798        delta: &mut DeltaCollector,
14799    ) -> Result<EvalResult, ExcelError> {
14800        let _source_cache = self.source_cache_session();
14801        if self.config.defer_graph_building {
14802            self.build_graph_all()?;
14803        }
14804        self.require_unified_authority()?;
14805        self.begin_evaluation_request();
14806        self.reset_virtual_dep_telemetry_if_disabled();
14807        let _span_eval = crate::engine::trace::fz_span!(
14808            tracing::Level::INFO,
14809            "evaluate",
14810            "evaluate.legacy_delta"
14811        );
14812        let start = crate::instant::FzInstant::now();
14813        let mut computed_vertices = 0;
14814        let mut cycle_errors = 0;
14815
14816        let mut replan_iterations = 0;
14817        const MAX_REPLAN: usize = 5;
14818        let mut telemetry = self
14819            .config
14820            .enable_virtual_dep_telemetry
14821            .then(|| self.start_virtual_dep_telemetry());
14822
14823        loop {
14824            let to_evaluate = self.graph.get_evaluation_vertices();
14825            if to_evaluate.is_empty() {
14826                if let Some(t) = telemetry.as_mut()
14827                    && t.bailout_reason.is_none()
14828                {
14829                    t.bailout_reason = Some("no_work");
14830                }
14831                break;
14832            }
14833
14834            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
14835            if let Some(t) = telemetry.as_mut() {
14836                Self::accumulate_schedule_meta(t, &meta);
14837            }
14838
14839            self.begin_pass(&schedule);
14840            for (unit_index, &unit) in schedule.units.iter().enumerate() {
14841                match unit {
14842                    ScheduleUnit::Cycle(i) => {
14843                        if self.handle_cycle_unit(
14844                            schedule.unit_cycle(i),
14845                            Some(delta),
14846                            None,
14847                            None,
14848                        )? > 0
14849                        {
14850                            cycle_errors += 1;
14851                        }
14852                    }
14853                    ScheduleUnit::Layer(i) => {
14854                        let layer = schedule.unit_layer(i);
14855                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14856                            computed_vertices +=
14857                                self.evaluate_layer_parallel_with_delta(layer, delta)?;
14858                        } else {
14859                            computed_vertices +=
14860                                self.evaluate_layer_sequential_with_delta(layer, delta)?;
14861                        }
14862                    }
14863                }
14864                if self.stop_after_unit(&schedule, unit_index) {
14865                    break;
14866                }
14867            }
14868
14869            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
14870            if let Some(t) = telemetry.as_mut() {
14871                t.changed_vdeps_total += changed_vertices.len();
14872            }
14873            self.resource_checkpoint(0)?;
14874            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
14875                if let Some(t) = telemetry.as_mut() {
14876                    t.bailout_reason = Some("converged");
14877                }
14878                break;
14879            }
14880            if replan_iterations >= MAX_REPLAN {
14881                if let Some(mut t) = telemetry.take() {
14882                    t.bailout_reason = Some("max_replan");
14883                    t.replan_iterations = replan_iterations;
14884                    self.last_virtual_dep_telemetry = t;
14885                }
14886                return Err(
14887                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
14888                );
14889            }
14890            replan_iterations += 1;
14891        }
14892
14893        if let Some(mut t) = telemetry {
14894            t.replan_iterations = replan_iterations;
14895            self.last_virtual_dep_telemetry = t;
14896        }
14897
14898        self.redirty_for_next_recalc();
14899        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
14900
14901        Ok(EvalResult {
14902            computed_vertices,
14903            cycle_errors,
14904            elapsed: start.elapsed(),
14905        })
14906    }
14907
14908    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
14909    ///
14910    /// This will evaluate only the minimal set of dirty / volatile precedents required
14911    /// to bring the target cell up-to-date (as if a user asked for that single value),
14912    /// rather than scheduling a full workbook recalc. If the cell is already clean and
14913    /// non-volatile, no vertices will be recomputed.
14914    ///
14915    /// Returns the (possibly newly computed) value stored for the cell afterwards.
14916    /// Empty cells return None. Errors are surfaced via the Result type.
14917    pub fn evaluate_cell(
14918        &mut self,
14919        sheet: &str,
14920        row: u32,
14921        col: u32,
14922    ) -> Result<Option<LiteralValue>, ExcelError> {
14923        self.observe_evaluation_resource_request(EvaluationRequestKind::Cell, |engine| {
14924            engine.evaluate_cell_unobserved(sheet, row, col)
14925        })
14926    }
14927
14928    fn evaluate_cell_unobserved(
14929        &mut self,
14930        sheet: &str,
14931        row: u32,
14932        col: u32,
14933    ) -> Result<Option<LiteralValue>, ExcelError> {
14934        if row == 0 || col == 0 {
14935            return Err(ExcelError::new(ExcelErrorKind::Ref)
14936                .with_message("Row and column must be >= 1".to_string()));
14937        }
14938
14939        let result = self.evaluate_cells(&[(sheet, row, col)])?;
14940
14941        match result.len() {
14942            0 => Ok(None),
14943            1 => {
14944                let v = result.into_iter().next().unwrap();
14945                Ok(v)
14946            }
14947            _ => unreachable!("evaluate_cells returned unexpected length"),
14948        }
14949    }
14950
14951    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
14952    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
14953    /// is computed once and evaluated, which is typically faster than calling
14954    /// `evaluate_cell` repeatedly for a related set of targets.
14955    ///
14956    /// Returns the resulting values for each requested target in the same order.
14957    pub fn evaluate_cells(
14958        &mut self,
14959        targets: &[(&str, u32, u32)],
14960    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14961        self.observe_evaluation_resource_request(EvaluationRequestKind::Cells, |engine| {
14962            engine.evaluate_cells_unobserved(targets)
14963        })
14964    }
14965
14966    fn evaluate_cells_unobserved(
14967        &mut self,
14968        targets: &[(&str, u32, u32)],
14969    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14970        self.observe_function_semantic_epoch()?;
14971        debug_assert!(
14972            !self.graph.deferred_dirty_active(),
14973            "deferred-dirty scope leaked into evaluate_cells: a begin_deferred_dirty \
14974             was not balanced by end_deferred_dirty"
14975        );
14976        self.validate_deterministic_mode()?;
14977        if targets.is_empty() {
14978            return Ok(Vec::new());
14979        }
14980        let typed_targets = self.legacy_coordinate_targets(targets);
14981        self.evaluate_mixed_targets(&typed_targets, None)?;
14982        Ok(targets
14983            .iter()
14984            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
14985            .collect())
14986    }
14987
14988    pub fn evaluate_cells_cancellable(
14989        &mut self,
14990        targets: &[(&str, u32, u32)],
14991        cancel: crate::engine::CancelToken,
14992    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
14993        self.observe_evaluation_resource_request(
14994            EvaluationRequestKind::CellsCancellable,
14995            |engine| {
14996                engine.observe_function_semantic_epoch()?;
14997                engine.active_cancel_flag = Some(cancel.clone());
14998                let res = engine.evaluate_cells_cancellable_impl(targets, cancel.as_flag());
14999                engine.active_cancel_flag = None;
15000                res
15001            },
15002        )
15003    }
15004
15005    fn evaluate_cells_cancellable_impl(
15006        &mut self,
15007        targets: &[(&str, u32, u32)],
15008        cancel_flag: &AtomicBool,
15009    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
15010        self.validate_deterministic_mode()?;
15011        if targets.is_empty() {
15012            return Ok(Vec::new());
15013        }
15014        if cancel_flag.load(Ordering::Relaxed) {
15015            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
15016                .with_message("Evaluation cancelled before target preparation"));
15017        }
15018        let typed_targets = self.legacy_coordinate_targets(targets);
15019        self.evaluate_mixed_targets(&typed_targets, None)?;
15020        Ok(targets
15021            .iter()
15022            .map(|(sheet, row, col)| self.get_cell_value(sheet, *row, *col))
15023            .collect())
15024    }
15025
15026    pub fn evaluate_cells_with_target_delta(
15027        &mut self,
15028        targets: &[(&str, u32, u32)],
15029    ) -> Result<(Vec<Option<LiteralValue>>, crate::engine::TargetEvalDelta), ExcelError> {
15030        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
15031            engine.observe_function_semantic_epoch()?;
15032            engine.validate_deterministic_mode()?;
15033            if targets.is_empty() {
15034                return Ok((Vec::new(), crate::engine::TargetEvalDelta::default()));
15035            }
15036            let mut collector = DeltaCollector::new(DeltaMode::Cells);
15037            engine.evaluate_until_with_delta_collector(targets, &mut collector)?;
15038            let values = targets
15039                .iter()
15040                .map(|(sheet, row, col)| engine.get_cell_value(sheet, *row, *col))
15041                .collect();
15042            Ok((values, collector.finish_target()))
15043        })
15044    }
15045
15046    pub fn evaluate_cells_with_delta(
15047        &mut self,
15048        targets: &[(&str, u32, u32)],
15049    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15050        self.evaluate_cells_with_delta_policy(targets, EvalDeltaCompatibilityPolicy::Unlimited)
15051    }
15052
15053    pub fn evaluate_cells_with_delta_policy(
15054        &mut self,
15055        targets: &[(&str, u32, u32)],
15056        policy: EvalDeltaCompatibilityPolicy,
15057    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15058        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
15059            engine.evaluate_cells_with_delta_unobserved(targets, policy)
15060        })
15061    }
15062
15063    fn evaluate_cells_with_delta_unobserved(
15064        &mut self,
15065        targets: &[(&str, u32, u32)],
15066        policy: EvalDeltaCompatibilityPolicy,
15067    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15068        self.observe_function_semantic_epoch()?;
15069        self.validate_deterministic_mode()?;
15070        if targets.is_empty() {
15071            return Ok((Vec::new(), EvalDelta::default()));
15072        }
15073        let mut collector = DeltaCollector::new(DeltaMode::Cells);
15074        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
15075        let values = targets
15076            .iter()
15077            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
15078            .collect();
15079        Ok((values, collector.finish_with_policy(policy)?))
15080    }
15081
15082    /// Get the evaluation plan for target cells without actually evaluating them
15083    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
15084        if targets.is_empty() {
15085            return Ok(EvalPlan {
15086                total_vertices_to_evaluate: 0,
15087                layers: Vec::new(),
15088                cycles_detected: 0,
15089                dirty_count: 0,
15090                volatile_count: 0,
15091                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15092                estimated_parallel_layers: 0,
15093                target_cells: Vec::new(),
15094            });
15095        }
15096        if self.config.defer_graph_building && self.has_staged_formulas() {
15097            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
15098                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
15099            ));
15100        }
15101
15102        // Convert targets to A1 notation for consistency
15103        let addresses: Vec<String> = targets
15104            .iter()
15105            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
15106            .collect();
15107
15108        // Parse target cell addresses
15109        let mut target_addrs = Vec::new();
15110        for (sheet, row, col) in targets {
15111            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
15112                let coord = Coord::from_excel(*row, *col, true, true);
15113                target_addrs.push(CellRef::new(sheet_id, coord));
15114            }
15115        }
15116
15117        // Find vertex IDs for targets
15118        let mut target_vertex_ids = Vec::new();
15119        for addr in &target_addrs {
15120            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
15121                target_vertex_ids.push(vertex_id);
15122            }
15123        }
15124
15125        if target_vertex_ids.is_empty() {
15126            return Ok(EvalPlan {
15127                total_vertices_to_evaluate: 0,
15128                layers: Vec::new(),
15129                cycles_detected: 0,
15130                dirty_count: 0,
15131                volatile_count: 0,
15132                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15133                estimated_parallel_layers: 0,
15134                target_cells: addresses,
15135            });
15136        }
15137
15138        // Build demand subgraph with virtual edges (same as evaluate_until)
15139        let (precedents_to_eval, vdeps) = self.demand_subgraph(&target_vertex_ids)?;
15140
15141        if precedents_to_eval.is_empty() {
15142            return Ok(EvalPlan {
15143                total_vertices_to_evaluate: 0,
15144                layers: Vec::new(),
15145                cycles_detected: 0,
15146                dirty_count: 0,
15147                volatile_count: 0,
15148                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15149                estimated_parallel_layers: 0,
15150                target_cells: addresses,
15151            });
15152        }
15153
15154        // Count dirty and volatile vertices
15155        let mut dirty_count = 0;
15156        let mut volatile_count = 0;
15157        for &vertex_id in &precedents_to_eval {
15158            if self.graph.is_dirty(vertex_id) {
15159                dirty_count += 1;
15160            }
15161            if self.graph.is_volatile(vertex_id) {
15162                volatile_count += 1;
15163            }
15164        }
15165
15166        // Create schedule for the minimal subgraph honoring virtual edges
15167        let schedule = self.create_authority_schedule(&precedents_to_eval, &vdeps, None)?;
15168
15169        // Build layer information
15170        let mut layers = Vec::new();
15171        let mut estimated_parallel_layers = 0;
15172        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
15173
15174        for layer in &schedule.layers {
15175            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
15176            if parallel_eligible {
15177                estimated_parallel_layers += 1;
15178            }
15179
15180            // Get sample cell addresses (up to 5)
15181            let sample_cells: Vec<String> = layer
15182                .vertices
15183                .iter()
15184                .take(5)
15185                .filter_map(|&vertex_id| {
15186                    self.graph
15187                        .get_cell_ref_for_vertex(vertex_id)
15188                        .map(|cell_ref| {
15189                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15190                            format!(
15191                                "{}!{}{}",
15192                                sheet_name,
15193                                Self::col_to_letters(cell_ref.coord.col().saturating_add(1)),
15194                                cell_ref.coord.row() + 1
15195                            )
15196                        })
15197                })
15198                .collect();
15199
15200            layers.push(LayerInfo {
15201                vertex_count: layer.vertices.len(),
15202                parallel_eligible,
15203                sample_cells,
15204            });
15205        }
15206
15207        Ok(EvalPlan {
15208            total_vertices_to_evaluate: precedents_to_eval.len(),
15209            layers,
15210            cycles_detected: schedule.cycles.len(),
15211            dirty_count,
15212            volatile_count,
15213            parallel_enabled,
15214            estimated_parallel_layers,
15215            target_cells: addresses,
15216        })
15217    }
15218    /// Helper to create a schedule, integrating virtual dependencies automatically.
15219    fn create_evaluation_schedule(
15220        &mut self,
15221        to_evaluate: &[VertexId],
15222    ) -> Result<EvaluationScheduleBuildOutput, ExcelError> {
15223        #[cfg(any(test, feature = "benchmark_internal"))]
15224        {
15225            self.recalc_reuse_probe.get_mut().unwrap().schedule_requests += 1;
15226        }
15227        // Fold pending edge deltas once per schedule build so traversal uses
15228        // the zero-allocation CSR slices (#125).
15229        #[cfg(any(test, feature = "legacy_oracle"))]
15230        self.graph.flush_pending_edge_deltas();
15231        // The cache key includes the authority revision: sync first.
15232        self.graph.authority_sync();
15233        if self.can_use_static_schedule_cache(to_evaluate) {
15234            // A recent schedule for the same request becomes the current one.
15235            let revision = self.schedule_cache_authority_revision();
15236            let current = |e: &CachedScheduleEntry| {
15237                e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15238            };
15239            if !self
15240                .cached_static_schedule
15241                .as_ref()
15242                .is_some_and(|c| current(c) && c.candidate_vertices.equals(to_evaluate))
15243                && let Some(i) = self.recent_schedules.iter().position(|e| {
15244                    current(e)
15245                        && e.candidate_vertices.len() == to_evaluate.len()
15246                        && e.candidate_vertices.equals(to_evaluate)
15247                })
15248            {
15249                let hit = self.recent_schedules.remove(i);
15250                if let Some(previous) = self.cached_static_schedule.replace(hit) {
15251                    self.retain_recent_schedule(previous);
15252                }
15253            }
15254            if let Some(cached) = self.cached_static_schedule.as_ref()
15255                && cached.topology_epoch == self.topology_epoch
15256                && cached.authority_revision == self.schedule_cache_authority_revision()
15257                && cached.candidate_vertices.equals(to_evaluate)
15258            {
15259                let meta = ScheduleBuildMeta {
15260                    candidate_vertices: to_evaluate.len(),
15261                    vdeps_vertices: 0,
15262                    vdeps_edges: 0,
15263                    builder_elapsed_ms: 0,
15264                    used_virtual_schedule: false,
15265                    schedule_cache_hit: true,
15266                    schedule_cache_eligible: true,
15267                };
15268                #[cfg(any(test, feature = "benchmark_internal"))]
15269                {
15270                    let mut probe = self.recalc_reuse_probe.lock().unwrap();
15271                    probe.schedule_cache_hits += 1;
15272                    probe.schedule_shared_handles += 1;
15273                }
15274                return Ok((
15275                    EvaluationSchedule::Shared(Arc::clone(&cached.schedule)),
15276                    FxHashMap::default(),
15277                    meta,
15278                ));
15279            }
15280
15281            let (schedule, vdeps, mut meta) = match self.schedule_from_base(to_evaluate)? {
15282                Some(schedule) => (
15283                    schedule,
15284                    FxHashMap::default(),
15285                    ScheduleBuildMeta {
15286                        candidate_vertices: to_evaluate.len(),
15287                        vdeps_vertices: 0,
15288                        vdeps_edges: 0,
15289                        builder_elapsed_ms: 0,
15290                        used_virtual_schedule: false,
15291                        schedule_cache_hit: false,
15292                        schedule_cache_eligible: true,
15293                    },
15294                ),
15295                None => self.create_evaluation_schedule_active(to_evaluate)?,
15296            };
15297            meta.schedule_cache_hit = false;
15298            meta.schedule_cache_eligible = true;
15299            #[cfg(any(test, feature = "benchmark_internal"))]
15300            {
15301                self.recalc_reuse_probe
15302                    .get_mut()
15303                    .unwrap()
15304                    .schedule_cache_misses += 1;
15305            }
15306            let schedule = if vdeps.is_empty() {
15307                // Clone previously discarded builder spare capacity. Keep that compact
15308                // retained payload while sharing it with the current request.
15309                let mut schedule = schedule;
15310                schedule.units.shrink_to_fit();
15311                for layer in &mut schedule.layers {
15312                    layer.vertices.shrink_to_fit();
15313                }
15314                schedule.layers.shrink_to_fit();
15315                for cycle in &mut schedule.cycles {
15316                    cycle.shrink_to_fit();
15317                }
15318                schedule.cycles.shrink_to_fit();
15319                let schedule = Arc::new(schedule);
15320                #[cfg(any(test, feature = "benchmark_internal"))]
15321                {
15322                    self.recalc_reuse_probe
15323                        .get_mut()
15324                        .unwrap()
15325                        .schedule_shared_handles += 1;
15326                }
15327                let entry = CachedScheduleEntry {
15328                    topology_epoch: self.topology_epoch,
15329                    authority_revision: self.schedule_cache_authority_revision(),
15330                    candidate_vertices: VertexIdRuns::from_slice(to_evaluate),
15331                    schedule: Arc::clone(&schedule),
15332                };
15333                if let Some(previous) = self.cached_static_schedule.replace(entry) {
15334                    self.retain_recent_schedule(previous);
15335                }
15336                EvaluationSchedule::Shared(schedule)
15337            } else {
15338                EvaluationSchedule::Owned(schedule)
15339            };
15340            return Ok((schedule, vdeps, meta));
15341        }
15342
15343        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_active(to_evaluate)?;
15344        meta.schedule_cache_hit = false;
15345        meta.schedule_cache_eligible = false;
15346        #[cfg(any(test, feature = "benchmark_internal"))]
15347        {
15348            self.recalc_reuse_probe
15349                .get_mut()
15350                .unwrap()
15351                .schedule_cache_ineligible += 1;
15352        }
15353        Ok((EvaluationSchedule::Owned(schedule), vdeps, meta))
15354    }
15355
15356    /// Plan reuse: the base schedule restricted to `to_evaluate` when it
15357    /// is current, covers the request, has at least
15358    /// `BASE_SCHEDULE_MIN_REQUEST` candidates, and is at most
15359    /// `BASE_SCHEDULE_RATIO` times its size (restricting walks the whole
15360    /// base; planning costs far more per candidate).
15361    fn schedule_from_base(
15362        &mut self,
15363        to_evaluate: &[VertexId],
15364    ) -> Result<Option<crate::engine::scheduler::Schedule>, ExcelError> {
15365        let revision = self.schedule_cache_authority_revision();
15366        let current = |e: &&CachedScheduleEntry| {
15367            e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15368        };
15369        // The base, or the current schedule when larger (the first
15370        // evaluation's, before a later request replaces it).
15371        let Some(base) = [
15372            self.base_schedule.as_ref(),
15373            self.cached_static_schedule.as_ref(),
15374        ]
15375        .into_iter()
15376        .flatten()
15377        .filter(current)
15378        .max_by_key(|e| e.candidate_vertices.len()) else {
15379            return Ok(None);
15380        };
15381        let base_len = base.candidate_vertices.len();
15382        if base_len <= BASE_SCHEDULE_MIN_VERTICES
15383            || to_evaluate.len() < BASE_SCHEDULE_MIN_REQUEST
15384            || to_evaluate.len() > base_len
15385            || base_len > to_evaluate.len().saturating_mul(BASE_SCHEDULE_RATIO)
15386        {
15387            return Ok(None);
15388        }
15389        let mut keep = crate::engine::idset::DenseIdSet::default();
15390        keep.extend(to_evaluate.iter().copied());
15391        let Some((schedule, kept)) = base.schedule.restrict(&keep) else {
15392            return Ok(None);
15393        };
15394        // Every requested vertex must be in the base.
15395        if kept != keep.len() {
15396            return Ok(None);
15397        }
15398        if let Some(ledger) = self.active_resource_ledger.as_mut() {
15399            let layers: usize = schedule
15400                .layers
15401                .iter()
15402                .map(|l| {
15403                    l.vertices.capacity() * std::mem::size_of::<VertexId>()
15404                        + l.runs.capacity()
15405                            * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
15406                })
15407                .sum();
15408            let bytes = (keep.heap_bytes()
15409                + layers
15410                + schedule.layers.capacity()
15411                    * std::mem::size_of::<crate::engine::scheduler::Layer>()
15412                + schedule.units.capacity()
15413                    * std::mem::size_of::<crate::engine::scheduler::ScheduleUnit>()
15414                + schedule
15415                    .cycles
15416                    .iter()
15417                    .map(|c| c.capacity() * 4)
15418                    .sum::<usize>()) as u64;
15419            ledger
15420                .reserve_schedule_discovery(bytes)
15421                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15422            ledger
15423                .release_scratch(bytes)
15424                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15425        }
15426        #[cfg(debug_assertions)]
15427        self.debug_check_restricted_schedule(to_evaluate, &schedule);
15428        #[cfg(any(test, feature = "benchmark_internal"))]
15429        {
15430            self.recalc_reuse_probe
15431                .get_mut()
15432                .unwrap()
15433                .schedule_base_restrictions += 1;
15434        }
15435        Ok(Some(schedule))
15436    }
15437
15438    /// Debug builds: a restricted schedule holds each requested vertex
15439    /// once and orders the request like a freshly planned schedule (every
15440    /// dependency among the request in an earlier unit).
15441    #[cfg(debug_assertions)]
15442    fn debug_check_restricted_schedule(
15443        &self,
15444        to_evaluate: &[VertexId],
15445        schedule: &crate::engine::scheduler::Schedule,
15446    ) {
15447        let mut position: FxHashMap<VertexId, usize> = FxHashMap::default();
15448        for (u, unit) in schedule.units.iter().enumerate() {
15449            let vertices: &[VertexId] = match *unit {
15450                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
15451                    &schedule.unit_layer(i).vertices
15452                }
15453                crate::engine::scheduler::ScheduleUnit::Cycle(i) => schedule.unit_cycle(i),
15454            };
15455            for &v in vertices {
15456                assert!(
15457                    position.insert(v, u).is_none(),
15458                    "vertex twice in a restricted schedule"
15459                );
15460            }
15461        }
15462        assert_eq!(position.len(), to_evaluate.len());
15463        if to_evaluate.len() > 512 {
15464            return;
15465        }
15466        // Every arc among the request (the store's edge images) goes to an
15467        // earlier unit, or within one sequential layer (a chain) or cycle.
15468        let Ok(store) = self.graph.authority_plan_store() else {
15469            return;
15470        };
15471        let cells: Vec<(VertexId, (u16, u32, u32))> = to_evaluate
15472            .iter()
15473            .filter_map(|&v| self.graph.authority_cell_of_vertex(v).map(|c| (v, c)))
15474            .collect();
15475        for &(v, (sheet, row, col)) in &cells {
15476            let Some(owner) = store.owner_at((sheet, row, col)) else {
15477                continue;
15478            };
15479            let Ok(refined) = store.refine_owner_column(owner, col, row, row, None) else {
15480                continue;
15481            };
15482            for piece in &refined.pieces {
15483                for edge in &refined.edges[piece.edge_start..piece.edge_end] {
15484                    let Some(image) = edge.proj.forward(&piece.domain) else {
15485                        continue;
15486                    };
15487                    for &(d, (s2, r2, c2)) in &cells {
15488                        if d == v
15489                            || edge.proj.sheet != s2
15490                            || !(image.r0 <= r2
15491                                && r2 <= image.r1
15492                                && image.c0 <= c2
15493                                && c2 <= image.c1)
15494                        {
15495                            continue;
15496                        }
15497                        let (pd, pv) = (position[&d], position[&v]);
15498                        let same_ok = pd == pv
15499                            && match schedule.units[pv] {
15500                                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
15501                                    schedule.unit_layer(i).sequential
15502                                }
15503                                crate::engine::scheduler::ScheduleUnit::Cycle(_) => true,
15504                            };
15505                        assert!(
15506                            pd < pv || same_ok,
15507                            "restricted schedule orders {v:?} before its precedent {d:?}"
15508                        );
15509                    }
15510                }
15511            }
15512        }
15513    }
15514
15515    /// Compress family formulas once per authority build (see
15516    /// `EvalConfig::formula_compression`), when no staged or deferred
15517    /// formula package can hold arena ids.
15518    fn maybe_compress_formulas(&mut self) {
15519        if !self.config.formula_compression {
15520            return;
15521        }
15522        let builds = self.graph.authority_host().builds;
15523        if self.compressed_at_build == Some(builds) {
15524            return;
15525        }
15526        self.compressed_at_build = Some(builds);
15527        if self.has_staged_formulas() {
15528            // Staged packages hold arena ids: no compaction. Members that
15529            // are already compressed can still leave the per-cell maps.
15530            self.graph.virtualize_family_members();
15531            return;
15532        }
15533        let pool = self.thread_pool.clone();
15534        let (_, garbage) = self.graph.compress_family_formulas(pool.as_deref());
15535        self.graph.virtualize_family_members();
15536        // Freeing the dropped members' reference texts (one allocation each)
15537        // is most of compaction; with a pool it happens off the critical
15538        // path.
15539        match pool {
15540            Some(pool) if garbage.len() >= 1024 => pool.spawn(move || drop(garbage)),
15541            _ => drop(garbage),
15542        }
15543    }
15544
15545    fn create_evaluation_schedule_active(
15546        &mut self,
15547        to_evaluate: &[VertexId],
15548    ) -> Result<ScheduleBuildOutput, ExcelError> {
15549        self.graph.authority_sync();
15550        self.maybe_compress_formulas();
15551        let mut ledger = self.active_resource_ledger.take();
15552        let result = self.create_evaluation_schedule_uncached(to_evaluate, ledger.as_mut());
15553        self.active_resource_ledger = ledger;
15554        result
15555    }
15556
15557    fn create_evaluation_schedule_uncached(
15558        &self,
15559        to_evaluate: &[VertexId],
15560        #[allow(unused_variables)] ledger: Option<&mut ResourceLedger>,
15561    ) -> Result<ScheduleBuildOutput, ExcelError> {
15562        #[cfg(any(test, feature = "benchmark_internal"))]
15563        {
15564            self.recalc_reuse_probe.lock().unwrap().schedule_builds += 1;
15565        }
15566        let builder = VirtualDepBuilder::new(self);
15567        #[allow(unused_mut)]
15568        let (mut vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
15569            if self.config.enable_virtual_dep_telemetry {
15570                let build_started = crate::instant::FzInstant::now();
15571                let (vdeps, augmented) = builder.build(to_evaluate);
15572                let builder_elapsed_ms = build_started.elapsed().as_millis();
15573                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
15574                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
15575            } else {
15576                let (vdeps, augmented) = builder.build(to_evaluate);
15577                (vdeps, augmented, 0, 0)
15578            };
15579
15580        // Replan hints from stale dynamic reads earlier in this request.
15581        {
15582            self.freshness_merge_hints(to_evaluate, &mut vdeps);
15583            self.freshness_extent_hints(to_evaluate, &mut vdeps);
15584        }
15585        let mut final_evaluate = to_evaluate.to_vec();
15586        if !augmented.is_empty() {
15587            final_evaluate.extend(augmented);
15588            final_evaluate.sort_unstable();
15589            final_evaluate.dedup();
15590        }
15591
15592        let use_virtual = !vdeps.is_empty();
15593
15594        let schedule = self.create_authority_schedule(&final_evaluate, &vdeps, ledger)?;
15595
15596        let meta = ScheduleBuildMeta {
15597            candidate_vertices: to_evaluate.len(),
15598            vdeps_vertices: vdeps.len(),
15599            vdeps_edges,
15600            builder_elapsed_ms,
15601            used_virtual_schedule: use_virtual,
15602            schedule_cache_hit: false,
15603            schedule_cache_eligible: false,
15604        };
15605
15606        Ok((schedule, vdeps, meta))
15607    }
15608
15609    fn create_authority_schedule(
15610        &self,
15611        candidates: &[VertexId],
15612        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
15613        mut ledger: Option<&mut ResourceLedger>,
15614    ) -> Result<crate::engine::scheduler::Schedule, ExcelError> {
15615        use crate::engine::authority::{
15616            geom::{Cover, Rect},
15617            plan_schedule, planner,
15618            proj::{AxisMap, RefProj},
15619            store::{EdgeKey, Tag},
15620        };
15621        let failure = |message: String| {
15622            ExcelError::new(ExcelErrorKind::Error)
15623                .with_message(format!("unified_authority planner: {message}"))
15624        };
15625        self.cancellation_checkpoint("Evaluation cancelled before authority planning")?;
15626        let store = self
15627            .graph
15628            .authority_plan_store()
15629            .map_err(Self::authority_excel_error)?;
15630        // One formula cell without hints (a tiny edit): its plan is the cell
15631        // alone unless it reads itself (`planner::plan_single`).
15632        if let [only] = candidates
15633            && vdeps.is_empty()
15634            && self.graph.authority_host().observed(*only).is_none()
15635            && let Some(cell) = self.graph.authority_cell_of_vertex(*only)
15636            && cell.0 != crate::engine::authority::geom::SYMBOL_SHEET
15637            && let Some(single) = planner::plan_single(store, cell)
15638        {
15639            #[cfg(debug_assertions)]
15640            {
15641                let mut cover = Cover::new();
15642                cover.insert_rect(cell.0, &Rect::new(cell.1, cell.2, cell.1, cell.2));
15643                let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
15644                    .expect("general plan of one cell");
15645                assert_eq!(
15646                    general.cells.as_slice(),
15647                    &[single],
15648                    "single-cell plan differs from the planner at {cell:?}"
15649                );
15650            }
15651            let adapted = plan_schedule::schedule(
15652                &[single],
15653                0,
15654                None,
15655                |cell| {
15656                    self.graph
15657                        .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15658                        .ok_or_else(|| failure("missing executor identity".to_owned()))
15659                },
15660                |_work| Ok(()),
15661            )
15662            .map_err(|error| match error {
15663                plan_schedule::ScheduleError::Runtime(error) => error,
15664                other => failure(format!("{other:?}")),
15665            })?;
15666            if let Some(ledger) = ledger {
15667                ledger
15668                    .reserve_schedule_discovery(adapted.peak_heap_bytes)
15669                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15670                ledger
15671                    .release_scratch(adapted.peak_heap_bytes)
15672                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15673            }
15674            return Ok(adapted.schedule);
15675        }
15676        // A small request of grid formula cells without hints: per-cell
15677        // arcs and longest-path levels (`planner::plan_small`).
15678        if (2..=planner::SMALL_PLAN_MAX).contains(&candidates.len())
15679            && vdeps.is_empty()
15680            && candidates
15681                .iter()
15682                .all(|&v| self.graph.authority_host().observed(v).is_none())
15683        {
15684            let cells: Option<Vec<(u16, u32, u32)>> = candidates
15685                .iter()
15686                .map(|&v| self.graph.authority_cell_of_vertex(v))
15687                .collect();
15688            if let Some(small) = cells.as_deref().and_then(|c| planner::plan_small(store, c)) {
15689                #[cfg(debug_assertions)]
15690                {
15691                    let mut cover = Cover::new();
15692                    for c in cells.as_deref().unwrap_or_default() {
15693                        cover.insert_rect(c.0, &Rect::new(c.1, c.2, c.1, c.2));
15694                    }
15695                    let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
15696                        .expect("general plan of a small request");
15697                    let key = |c: &planner::OrderedCell| (c.sheet, c.row, c.col, c.id, c.owner);
15698                    let mut a: Vec<_> = general.cells.iter().map(key).collect();
15699                    let mut b: Vec<_> = small.iter().map(key).collect();
15700                    a.sort_unstable();
15701                    b.sort_unstable();
15702                    assert_eq!(a, b, "small plan cells differ from the planner");
15703                    assert!(
15704                        general.cells.iter().all(|c| c.cycle.is_none()),
15705                        "small plan of a cyclic request"
15706                    );
15707                }
15708                let adapted = plan_schedule::schedule(
15709                    &small,
15710                    0,
15711                    None,
15712                    |cell| {
15713                        self.graph
15714                            .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15715                            .ok_or_else(|| failure("missing executor identity".to_owned()))
15716                    },
15717                    |_work| Ok(()),
15718                )
15719                .map_err(|error| match error {
15720                    plan_schedule::ScheduleError::Runtime(error) => error,
15721                    other => failure(format!("{other:?}")),
15722                })?;
15723                if let Some(ledger) = ledger {
15724                    ledger
15725                        .reserve_schedule_discovery(adapted.peak_heap_bytes)
15726                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15727                    ledger
15728                        .release_scratch(adapted.peak_heap_bytes)
15729                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15730                }
15731                return Ok(adapted.schedule);
15732            }
15733        }
15734        // Names are symbol-plane nodes (design §4.1): a name vertex plans as
15735        // the unit at its node, between its precedents and its readers.
15736        // Candidates become cells, sorted by (sheet, column, row) and
15737        // coalesced into row intervals: one cover insert per interval.
15738        let mut cover = Cover::new();
15739        {
15740            let cell_of = |&id: &VertexId| {
15741                self.graph
15742                    .authority_cell_of_vertex(id)
15743                    .map(|(sheet, row, col)| (sheet, col, row))
15744            };
15745            // A full recalc maps and sorts every formula: on the pool when
15746            // there is one (first eval's schedule is serial work otherwise).
15747            let cells: Vec<(u16, u32, u32)> = match self.thread_pool.as_deref() {
15748                Some(pool) if candidates.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
15749                    use rayon::prelude::*;
15750                    pool.install(|| {
15751                        let mut cells: Vec<_> = candidates.par_iter().filter_map(cell_of).collect();
15752                        cells.par_sort_unstable();
15753                        cells
15754                    })
15755                }
15756                _ => {
15757                    let mut cells: Vec<_> = candidates.iter().filter_map(cell_of).collect();
15758                    cells.sort_unstable();
15759                    cells
15760                }
15761            };
15762            let mut i = 0;
15763            while i < cells.len() {
15764                let (sheet, col, r0) = cells[i];
15765                let mut r1 = r0;
15766                let mut j = i + 1;
15767                while j < cells.len() && cells[j].0 == sheet && cells[j].1 == col {
15768                    if cells[j].2 > r1 + 1 {
15769                        break;
15770                    }
15771                    r1 = r1.max(cells[j].2);
15772                    j += 1;
15773                }
15774                cover.insert_rect(sheet, &Rect::new(r0, col, r1, col));
15775                i = j;
15776            }
15777        }
15778        let mut hints = Vec::new();
15779        for (&reader, deps) in vdeps {
15780            let Some(reader) = self.graph.authority_cell_of_vertex(reader) else {
15781                continue;
15782            };
15783            for &dependency in deps {
15784                let Some(dep) = self.graph.authority_cell_of_vertex(dependency) else {
15785                    continue;
15786                };
15787                hints.push(planner::PlanHint {
15788                    reader: (reader.0, reader.2, reader.1),
15789                    edge: EdgeKey {
15790                        dep_sheet: reader.0,
15791                        tag: Tag::X,
15792                        lk: u32::MAX,
15793                        proj: RefProj {
15794                            sheet: dep.0,
15795                            rows: AxisMap::fixed(dep.1, dep.1),
15796                            cols: AxisMap::fixed(dep.2, dep.2),
15797                        },
15798                    },
15799                });
15800            }
15801        }
15802        // rdi_dyn: order each dynamic reader after its observed reads.
15803        let host = self.graph.authority_host();
15804        for &id in candidates.iter().filter(|_| host.has_observed()) {
15805            let Some(reads) = host.observed(id) else {
15806                continue;
15807            };
15808            let Some(reader) = self.graph.authority_cell_of_vertex(id) else {
15809                continue;
15810            };
15811            for &(sheet, r0, c0, r1, c1) in reads {
15812                hints.push(planner::PlanHint {
15813                    reader: (reader.0, reader.2, reader.1),
15814                    edge: EdgeKey {
15815                        dep_sheet: reader.0,
15816                        tag: Tag::X,
15817                        lk: u32::MAX,
15818                        proj: RefProj {
15819                            sheet,
15820                            rows: AxisMap::fixed(r0, r1),
15821                            cols: AxisMap::fixed(c0, c1),
15822                        },
15823                    },
15824                });
15825            }
15826        }
15827        hints.sort_unstable_by_key(|hint| hint.reader);
15828        let checkpoint = ledger
15829            .as_ref()
15830            .map_or(0, |ledger| ledger.scratch_checkpoint());
15831        let scratch_limit = ledger
15832            .as_ref()
15833            .and_then(|ledger| ledger.schedule_discovery_limit())
15834            .map(|limit| limit.saturating_sub(checkpoint));
15835        let ordered = planner::plan_with_hints(store, &cover, &hints, scratch_limit, None, None)
15836            .map_err(|error| failure(format!("{error:?}")))?;
15837        // max_work_units is an execution budget. Planning work must be capped
15838        // independently: charging it here changes the observable publication
15839        // boundary (e.g. a spill must commit before the next execution fails).
15840        let adapted = plan_schedule::schedule(
15841            &ordered.cells,
15842            ordered.heap_bytes(),
15843            scratch_limit,
15844            |cell| {
15845                self.graph
15846                    .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
15847                    .ok_or_else(|| failure("missing executor identity".to_owned()))
15848            },
15849            |_work| {
15850                self.cancellation_checkpoint("Evaluation cancelled during authority planning")?;
15851                if let Some(ledger) = ledger.as_deref_mut() {
15852                    ledger
15853                        .checkpoint_deadline()
15854                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15855                }
15856                Ok(())
15857            },
15858        )
15859        .map_err(|error| match error {
15860            plan_schedule::ScheduleError::Runtime(error) => error,
15861            other => failure(format!("{other:?}")),
15862        })?;
15863        if let Some(ledger) = ledger {
15864            let peak = ordered.peak_heap_bytes.max(adapted.peak_heap_bytes);
15865            ledger
15866                .reserve_schedule_discovery(peak)
15867                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15868            ledger
15869                .release_scratch(peak)
15870                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15871        }
15872        // The planner's order is scratch now; a full recalc's is large.
15873        match self.thread_pool.as_deref() {
15874            Some(pool) if ordered.cells.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
15875                pool.spawn(move || drop(ordered))
15876            }
15877            _ => drop(ordered),
15878        }
15879        Ok(adapted.schedule)
15880    }
15881
15882    /// Static-schedule cache eligibility. Legacy excludes range readers:
15883    /// their order comes from per-request range virtual deps. Under the
15884    /// authority a range read is a static edge of the relation, so only
15885    /// dynamic readers (whose hints are per request) are excluded, and the
15886    /// key adds the authority revision (design §8.4).
15887    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
15888        {
15889            // A dynamic reader is planned from its observed reads, which the
15890            // key covers (rev.dyn); one without them needs a pre-probe, and
15891            // request-scoped replan hints are never cached.
15892            let host = self.graph.authority_host();
15893            !to_evaluate.is_empty()
15894                && !self.freshness_has_hints()
15895                && to_evaluate
15896                    .iter()
15897                    .all(|&v| !self.graph.is_dynamic(v) || host.observed(v).is_some())
15898        }
15899    }
15900
15901    fn schedule_cache_authority_revision(&self) -> (u64, u64) {
15902        {
15903            // rev.topology is `topology_epoch`; a symbol revision rebuilds the
15904            // store, so it is part of `revision`.
15905            let host = self.graph.authority_host();
15906            (host.revision(), host.rev_dyn())
15907        }
15908    }
15909
15910    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
15911        VirtualDepTelemetry {
15912            fallback_mode_activations: self.virtual_dep_fallback_activations,
15913            ..VirtualDepTelemetry::default()
15914        }
15915    }
15916
15917    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
15918        telemetry.candidate_vertices_total += meta.candidate_vertices;
15919        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
15920        telemetry.vdeps_edges_total += meta.vdeps_edges;
15921        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
15922        if meta.schedule_cache_eligible {
15923            if meta.schedule_cache_hit {
15924                telemetry.schedule_cache_hits += 1;
15925                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
15926            } else {
15927                telemetry.schedule_cache_misses += 1;
15928            }
15929        }
15930        if meta.used_virtual_schedule {
15931            telemetry.schedule_virtual_passes += 1;
15932        } else {
15933            telemetry.schedule_static_passes += 1;
15934        }
15935    }
15936
15937    /// End-of-pass dirty bookkeeping; true when the loop must replan.
15938    /// Legacy: clear the pass, re-dirty readers whose pre-probe changed.
15939    /// Under the authority an armed pass keeps stale and unreached vertices
15940    /// dirty instead (design §8.2, `freshness.rs`).
15941    fn finish_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
15942        self.finish_pass_dirty_scoped(to_evaluate, changed, true)
15943    }
15944
15945    /// [`Self::finish_pass_dirty`] for a targeted pass: only its candidates
15946    /// decide whether to replan.
15947    fn finish_target_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
15948        self.finish_pass_dirty_scoped(to_evaluate, changed, false)
15949    }
15950
15951    fn finish_pass_dirty_scoped(
15952        &mut self,
15953        to_evaluate: &[VertexId],
15954        changed: &[VertexId],
15955        #[allow(unused_variables)] whole_workbook: bool,
15956    ) -> bool {
15957        self.freshness_finish_pass(to_evaluate, changed, whole_workbook)
15958    }
15959
15960    /// Start a pass over `schedule` (arms the freshness recorder).
15961    fn begin_pass(
15962        &mut self,
15963        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
15964    ) {
15965        self.freshness_begin_pass(schedule);
15966    }
15967
15968    /// FR4 layer barrier after unit `index`: true stops the pass.
15969    fn stop_after_unit(
15970        &mut self,
15971        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
15972        #[allow(unused_variables)] index: usize,
15973    ) -> bool {
15974        self.freshness_stop_after_unit(schedule, index)
15975    }
15976
15977    fn changed_virtual_dep_vertices(
15978        &mut self,
15979        to_evaluate: &[VertexId],
15980        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
15981    ) -> Vec<VertexId> {
15982        #[cfg(test)]
15983        if self.force_virtual_dep_changes_remaining_for_test > 0
15984            && let Some(vertex) = to_evaluate.first().copied()
15985        {
15986            self.force_virtual_dep_changes_remaining_for_test -= 1;
15987            return vec![vertex];
15988        }
15989        // An armed pass detects stale dynamic reads directly; the pre-probe
15990        // comparison is legacy's substitute for that (design §8.2).
15991        if self.freshness_armed() {
15992            return Vec::new();
15993        }
15994        if !to_evaluate
15995            .iter()
15996            .copied()
15997            .any(|v| self.graph.is_dynamic(v))
15998        {
15999            return Vec::new();
16000        }
16001
16002        let builder = VirtualDepBuilder::new(self);
16003        let (new_vdeps, _) = builder.build(to_evaluate);
16004
16005        let mut candidates = FxHashSet::default();
16006        candidates.extend(old_vdeps.keys().copied());
16007        candidates.extend(new_vdeps.keys().copied());
16008
16009        let mut changed = Vec::new();
16010        for v in candidates {
16011            if old_vdeps.get(&v) != new_vdeps.get(&v) {
16012                changed.push(v);
16013            }
16014        }
16015        changed
16016    }
16017
16018    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
16019    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
16020    /// Demand candidates of `targets` and their dynamic plan hints: under
16021    /// `unified_authority` from the authority's relation (design §8.3),
16022    /// otherwise from the legacy graph.
16023    #[allow(clippy::type_complexity)]
16024    fn demand_subgraph(
16025        &self,
16026        targets: &[VertexId],
16027    ) -> Result<
16028        (
16029            Vec<VertexId>,
16030            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
16031        ),
16032        ExcelError,
16033    > {
16034        self.authority_demand_subgraph(targets)
16035    }
16036
16037    /// Design §8.3: traverse precedents from the targets over the
16038    /// authority's static relation (symbol nodes are ordinary pieces) plus
16039    /// the dynamic readers' virtual dependencies, and collect what legacy's
16040    /// demand walk collects: dirty or volatile formula cells and every name
16041    /// passed through. No legacy dependency structure is read.
16042    #[allow(clippy::type_complexity)]
16043    fn authority_demand_subgraph(
16044        &self,
16045        targets: &[VertexId],
16046    ) -> Result<
16047        (
16048            Vec<VertexId>,
16049            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
16050        ),
16051        ExcelError,
16052    > {
16053        use crate::engine::authority::geom::{Cell, Rect, SYMBOL_SHEET};
16054        use crate::engine::authority::store::TagFilter;
16055        use rustc_hash::{FxHashMap, FxHashSet};
16056        let store = self
16057            .graph
16058            .authority_plan_store()
16059            .map_err(Self::authority_excel_error)?;
16060        let ids = store.ids();
16061        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
16062        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
16063        let mut visited: FxHashSet<Cell> = FxHashSet::default();
16064        // (cell, authority id or NO_VID): the id lets the side array
16065        // translate the cell without a hash lookup.
16066        let mut stack: Vec<(Cell, u32)> = Vec::new();
16067        // Formula cells under `rect` on `sheet`: identity runs per column.
16068        let push_formulas = |stack: &mut Vec<(Cell, u32)>,
16069                             visited: &FxHashSet<Cell>,
16070                             sheet: u16,
16071                             rect: Rect| {
16072            for col in rect.c0..=rect.c1 {
16073                ids.visit_runs_in(sheet, col, rect.r0, rect.r1, &mut |h| {
16074                    let run = ids.run(h);
16075                    let r0 = run.row_start.max(rect.r0);
16076                    let r1 = (run.row_start + run.len - 1).min(rect.r1);
16077                    for row in r0..=r1 {
16078                        if !visited.contains(&(sheet, row, col)) {
16079                            stack.push(((sheet, row, col), run.first_id + (row - run.row_start)));
16080                        }
16081                    }
16082                });
16083            }
16084        };
16085        for &v in targets {
16086            if let Some(table) = self.graph.table_by_vertex(v) {
16087                // A table's demand is its range's, as legacy's table vertex
16088                // leads to the cells it covers (its symbol row has no
16089                // precedents).
16090                let (s, e) = (table.range.start, table.range.end);
16091                push_formulas(
16092                    &mut stack,
16093                    &visited,
16094                    s.sheet_id,
16095                    Rect::new(s.coord.row(), s.coord.col(), e.coord.row(), e.coord.col()),
16096                );
16097            } else if let Some(cell) = self.graph.authority_cell_of_vertex(v) {
16098                stack.push((cell, crate::engine::authority::identity::NO_VID));
16099            }
16100        }
16101        let mut hits = Vec::new();
16102        #[cfg(any(test, feature = "benchmark_internal"))]
16103        let (mut probe_vertices, mut probe_clean_formulas, mut probe_edges, mut probe_dynamic) =
16104            (0, 0, 0, 0);
16105        while let Some((cell, id)) = stack.pop() {
16106            if !visited.insert(cell) {
16107                continue;
16108            }
16109            let Some(v) = self.graph.authority_vertex_of_formula(id, cell) else {
16110                continue;
16111            };
16112            if !self.graph.vertex_exists(v) {
16113                continue;
16114            }
16115            #[cfg(any(test, feature = "benchmark_internal"))]
16116            {
16117                probe_vertices += 1;
16118            }
16119            match self.graph.get_vertex_kind(v) {
16120                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
16121                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
16122                        to_evaluate.insert(v);
16123                    } else {
16124                        #[cfg(any(test, feature = "benchmark_internal"))]
16125                        {
16126                            probe_clean_formulas += 1;
16127                        }
16128                    }
16129                }
16130                VertexKind::NamedScalar | VertexKind::NamedArray => {
16131                    to_evaluate.insert(v);
16132                }
16133                _ => {}
16134            }
16135            hits.clear();
16136            store.direct_precedents(cell, TagFilter::All, &mut hits);
16137            #[cfg(any(test, feature = "benchmark_internal"))]
16138            {
16139                probe_edges += hits.len();
16140            }
16141            for &(_, sheet, rect) in &hits {
16142                // DirtyExtents: a dirty spill anchor whose extent meets this
16143                // image is a demand precedent, ordered before `v` (§8.2).
16144                if sheet != SYMBOL_SHEET {
16145                    for anchor in self
16146                        .graph
16147                        .spill_anchors_in_region(sheet, rect.r0, rect.c0, rect.r1, rect.c1)
16148                    {
16149                        if anchor != v && self.graph.is_dirty(anchor) {
16150                            vdeps.entry(v).or_default().push(anchor);
16151                            if let Some(c) = self.graph.authority_cell_of_vertex(anchor) {
16152                                stack.push((c, crate::engine::authority::identity::NO_VID));
16153                            }
16154                        }
16155                    }
16156                }
16157                if sheet == SYMBOL_SHEET {
16158                    stack.extend((rect.r0..=rect.r1).map(|slot| {
16159                        (
16160                            (SYMBOL_SHEET, slot, 0),
16161                            crate::engine::authority::identity::NO_VID,
16162                        )
16163                    }));
16164                    continue;
16165                }
16166                push_formulas(&mut stack, &visited, sheet, rect);
16167            }
16168            if self.graph.is_dynamic(v) {
16169                #[cfg(any(test, feature = "benchmark_internal"))]
16170                {
16171                    probe_dynamic += 1;
16172                }
16173                // rdi_dyn: the observed reads are demand precedents.
16174                if let Some(reads) = self.graph.authority_host().observed(v) {
16175                    for &(sheet, r0, c0, r1, c1) in reads {
16176                        push_formulas(&mut stack, &visited, sheet, Rect::new(r0, c0, r1, c1));
16177                    }
16178                }
16179                let (vdeps_map, _) = VirtualDepBuilder::new(self).build(&[v]);
16180                // Pre-probe targets plus reads this request found dirty
16181                // (design §8.3: demand walks rdi ∪ hints).
16182                let hinted = self.freshness_hints(v).unwrap_or(&[]);
16183                if let Some(deps) = vdeps_map
16184                    .get(&v)
16185                    .map(|deps| deps.iter().chain(hinted))
16186                    .or(Some([].iter().chain(hinted)))
16187                {
16188                    for &u in deps {
16189                        vdeps.entry(v).or_default().push(u);
16190                        if let Some(c) = self.graph.authority_cell_of_vertex(u) {
16191                            stack.push((c, crate::engine::authority::identity::NO_VID));
16192                        }
16193                    }
16194                }
16195            }
16196        }
16197        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
16198        result.sort_unstable();
16199        for deps in vdeps.values_mut() {
16200            deps.sort_unstable();
16201            deps.dedup();
16202        }
16203        #[cfg(any(test, feature = "benchmark_internal"))]
16204        {
16205            let mut probe = self.recalc_reuse_probe.lock().unwrap();
16206            probe.demand_builds += 1;
16207            probe.demand_vertices += probe_vertices;
16208            probe.demand_clean_formulas += probe_clean_formulas;
16209            probe.demand_explicit_edges += probe_edges;
16210            probe.demand_virtual_builder_calls += probe_dynamic;
16211        }
16212        Ok((result, vdeps))
16213    }
16214
16215    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
16216    fn col_to_letters(col: u32) -> String {
16217        col_letters_from_1based(col).expect("column index must be >= 1")
16218    }
16219
16220    /// Evaluate all dirty/volatile vertices with cancellation support
16221    pub fn evaluate_all_cancellable(
16222        &mut self,
16223        cancel: crate::engine::CancelToken,
16224    ) -> Result<EvalResult, ExcelError> {
16225        self.observe_evaluation_resource_request(EvaluationRequestKind::FullCancellable, |engine| {
16226            engine.observe_function_semantic_epoch()?;
16227            engine.active_cancel_flag = Some(cancel.clone());
16228            let res = engine.evaluate_all_cancellable_impl(cancel.as_flag());
16229            engine.active_cancel_flag = None;
16230            res
16231        })
16232    }
16233
16234    fn evaluate_all_cancellable_impl(
16235        &mut self,
16236        cancel_flag: &AtomicBool,
16237    ) -> Result<EvalResult, ExcelError> {
16238        let _source_cache = self.source_cache_session();
16239        self.validate_deterministic_mode()?;
16240        if self.config.defer_graph_building {
16241            self.build_graph_all()?;
16242        }
16243        if cancel_flag.load(Ordering::Relaxed) {
16244            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16245                .with_message("Evaluation cancelled before scheduling".to_string()));
16246        }
16247        self.require_unified_authority()?;
16248        self.begin_evaluation_request();
16249        self.reset_virtual_dep_telemetry_if_disabled();
16250        let start = crate::instant::FzInstant::now();
16251        let mut computed_vertices = 0;
16252        let mut cycle_errors = 0;
16253
16254        let mut replan_iterations = 0;
16255        const MAX_REPLAN: usize = 5;
16256        let mut telemetry = self
16257            .config
16258            .enable_virtual_dep_telemetry
16259            .then(|| self.start_virtual_dep_telemetry());
16260
16261        loop {
16262            if cancel_flag.load(Ordering::Relaxed) {
16263                if let Some(mut t) = telemetry {
16264                    t.bailout_reason = Some("cancelled");
16265                    t.replan_iterations = replan_iterations;
16266                    self.last_virtual_dep_telemetry = t;
16267                }
16268                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16269                    .with_message("Evaluation cancelled before scheduling".to_string()));
16270            }
16271
16272            let to_evaluate = self.graph.get_evaluation_vertices();
16273            if to_evaluate.is_empty() {
16274                if let Some(t) = telemetry.as_mut()
16275                    && t.bailout_reason.is_none()
16276                {
16277                    t.bailout_reason = Some("no_work");
16278                }
16279                break;
16280            }
16281
16282            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16283            if let Some(t) = telemetry.as_mut() {
16284                Self::accumulate_schedule_meta(t, &meta);
16285            }
16286
16287            // Walk units in condensation order, checking cancellation between
16288            // units (formerly between cycles and between layers).
16289            self.begin_pass(&schedule);
16290            for (unit_index, &unit) in schedule.units.iter().enumerate() {
16291                match unit {
16292                    ScheduleUnit::Cycle(i) => {
16293                        // Check cancellation between cycles
16294                        if cancel_flag.load(Ordering::Relaxed) {
16295                            if let Some(mut t) = telemetry {
16296                                t.bailout_reason = Some("cancelled");
16297                                t.replan_iterations = replan_iterations;
16298                                self.last_virtual_dep_telemetry = t;
16299                            }
16300                            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
16301                                "Evaluation cancelled during cycle handling".to_string(),
16302                            ));
16303                        }
16304
16305                        if self.handle_cycle_unit(
16306                            schedule.unit_cycle(i),
16307                            None,
16308                            None,
16309                            Some(cancel_flag),
16310                        )? > 0
16311                        {
16312                            cycle_errors += 1;
16313                        }
16314                    }
16315                    ScheduleUnit::Layer(i) => {
16316                        let layer = schedule.unit_layer(i);
16317                        // Check cancellation between layers
16318                        if cancel_flag.load(Ordering::Relaxed) {
16319                            if let Some(mut t) = telemetry {
16320                                t.bailout_reason = Some("cancelled");
16321                                t.replan_iterations = replan_iterations;
16322                                self.last_virtual_dep_telemetry = t;
16323                            }
16324                            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16325                                .with_message("Evaluation cancelled between layers".to_string()));
16326                        }
16327
16328                        // Evaluate vertices in this layer (parallel or sequential)
16329                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16330                            computed_vertices +=
16331                                self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
16332                        } else {
16333                            computed_vertices +=
16334                                self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
16335                        }
16336                    }
16337                }
16338                if self.stop_after_unit(&schedule, unit_index) {
16339                    break;
16340                }
16341            }
16342
16343            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16344            if let Some(t) = telemetry.as_mut() {
16345                t.changed_vdeps_total += changed_vertices.len();
16346            }
16347            self.resource_checkpoint(0)?;
16348            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16349                if let Some(t) = telemetry.as_mut() {
16350                    t.bailout_reason = Some("converged");
16351                }
16352                break;
16353            }
16354            if replan_iterations >= MAX_REPLAN {
16355                if let Some(mut t) = telemetry.take() {
16356                    t.bailout_reason = Some("max_replan");
16357                    t.replan_iterations = replan_iterations;
16358                    self.last_virtual_dep_telemetry = t;
16359                }
16360                return Err(
16361                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16362                );
16363            }
16364            replan_iterations += 1;
16365        }
16366
16367        if let Some(mut t) = telemetry {
16368            t.replan_iterations = replan_iterations;
16369            self.last_virtual_dep_telemetry = t;
16370        }
16371
16372        // Re-dirty volatile vertices for the next evaluation cycle
16373        self.redirty_for_next_recalc();
16374        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16375
16376        Ok(EvalResult {
16377            computed_vertices,
16378            cycle_errors,
16379            elapsed: start.elapsed(),
16380        })
16381    }
16382
16383    /// Evaluate only the necessary precedents for specific target cells with cancellation support
16384    pub fn evaluate_until_cancellable(
16385        &mut self,
16386        targets: &[&str],
16387        cancel: crate::engine::CancelToken,
16388    ) -> Result<EvalResult, ExcelError> {
16389        self.observe_evaluation_resource_request(
16390            EvaluationRequestKind::TargetedCancellable,
16391            |engine| {
16392                engine.observe_function_semantic_epoch()?;
16393                engine.active_cancel_flag = Some(cancel.clone());
16394                let res = engine.evaluate_until_cancellable_impl(targets, cancel.as_flag());
16395                engine.active_cancel_flag = None;
16396                res
16397            },
16398        )
16399    }
16400
16401    fn evaluate_until_cancellable_impl(
16402        &mut self,
16403        targets: &[&str],
16404        cancel_flag: &AtomicBool,
16405    ) -> Result<EvalResult, ExcelError> {
16406        if cancel_flag.load(Ordering::Relaxed) {
16407            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16408                .with_message("Evaluation cancelled before target preparation"));
16409        }
16410        let mut typed_targets = Vec::with_capacity(targets.len());
16411        for target in targets {
16412            let (sheet, row, col) = self.parse_a1_notation(target)?;
16413            self.graph.sheet_id_mut(&sheet);
16414            typed_targets.push(crate::engine::EvaluationTarget::Cell { sheet, row, col });
16415        }
16416        self.evaluate_mixed_targets(&typed_targets, None)
16417    }
16418
16419    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
16420        let mut quoted = false;
16421        let mut separator = None;
16422        let bytes = address.as_bytes();
16423        let mut index = 0usize;
16424        while index < bytes.len() {
16425            match bytes[index] {
16426                b'\'' => {
16427                    if quoted && bytes.get(index + 1) == Some(&b'\'') {
16428                        index = index.saturating_add(1);
16429                    } else {
16430                        quoted = !quoted;
16431                    }
16432                }
16433                b'!' if !quoted => separator = Some(index),
16434                _ => {}
16435            }
16436            index = index.saturating_add(1);
16437        }
16438        if quoted {
16439            return Err(ExcelError::new(ExcelErrorKind::Ref)
16440                .with_message(format!("Invalid quoted sheet reference `{address}`")));
16441        }
16442        let (sheet, cell_part) = match separator {
16443            Some(separator) => {
16444                let raw_sheet = &address[..separator];
16445                let sheet = if raw_sheet.starts_with('\'') && raw_sheet.ends_with('\'') {
16446                    raw_sheet[1..raw_sheet.len().saturating_sub(1)].replace("''", "'")
16447                } else {
16448                    raw_sheet.to_string()
16449                };
16450                (sheet, &address[separator + 1..])
16451            }
16452            None => (self.default_sheet_name().to_string(), address),
16453        };
16454
16455        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
16456            ExcelError::new(ExcelErrorKind::Ref)
16457                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
16458        })?;
16459
16460        Ok((sheet, row, col))
16461    }
16462
16463    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
16464    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
16465        use formualizer_parse::parser::ASTNodeType;
16466        match &ast.node_type {
16467            ASTNodeType::Function { name, args, .. } => {
16468                if let Some(func) = self
16469                    .get_function("", name)
16470                    .or_else(|| crate::function_registry::get("", name))
16471                    && func.caps().contains(crate::function::FnCaps::VOLATILE)
16472                {
16473                    return true;
16474                }
16475                args.iter()
16476                    .any(|arg| self.is_ast_volatile_with_provider(arg))
16477            }
16478            ASTNodeType::BinaryOp { left, right, .. } => {
16479                self.is_ast_volatile_with_provider(left)
16480                    || self.is_ast_volatile_with_provider(right)
16481            }
16482            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
16483            ASTNodeType::Array(rows) => rows.iter().any(|row| {
16484                row.iter()
16485                    .any(|cell| self.is_ast_volatile_with_provider(cell))
16486            }),
16487            _ => false,
16488        }
16489    }
16490
16491    /// Evaluate a layer sequentially
16492    fn evaluate_layer_sequential(
16493        &mut self,
16494        layer: &super::scheduler::Layer,
16495    ) -> Result<usize, ExcelError> {
16496        self.resource_checkpoint(layer.vertices.len() as u64)?;
16497        self.evaluate_layer_sequential_effects(layer)
16498    }
16499
16500    fn update_vertex_value_with_delta(
16501        &mut self,
16502        vertex_id: VertexId,
16503        new_value: LiteralValue,
16504        delta: &mut DeltaCollector,
16505    ) {
16506        if delta.mode != DeltaMode::Off
16507            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
16508        {
16509            let sheet_name = self.graph.sheet_name(cell.sheet_id);
16510            let old = self
16511                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
16512                .unwrap_or(LiteralValue::Empty);
16513            if old != new_value {
16514                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
16515            }
16516        }
16517        self.graph.update_vertex_value_ref(vertex_id, &new_value);
16518        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
16519    }
16520
16521    fn evaluate_layer_sequential_with_delta(
16522        &mut self,
16523        layer: &super::scheduler::Layer,
16524        delta: &mut DeltaCollector,
16525    ) -> Result<usize, ExcelError> {
16526        self.resource_checkpoint(layer.vertices.len() as u64)?;
16527        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
16528    }
16529
16530    /// Evaluate a layer sequentially with cancellation support
16531    fn evaluate_layer_sequential_cancellable(
16532        &mut self,
16533        layer: &super::scheduler::Layer,
16534        cancel_flag: &AtomicBool,
16535    ) -> Result<usize, ExcelError> {
16536        self.resource_checkpoint(layer.vertices.len() as u64)?;
16537        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
16538    }
16539
16540    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
16541    fn evaluate_layer_sequential_cancellable_demand_driven(
16542        &mut self,
16543        layer: &super::scheduler::Layer,
16544        cancel_flag: &AtomicBool,
16545    ) -> Result<usize, ExcelError> {
16546        self.resource_checkpoint(layer.vertices.len() as u64)?;
16547        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
16548    }
16549
16550    /// Evaluate a layer in parallel using the thread pool.
16551    ///
16552    /// Cost-adaptive: the layer starts sequentially in slices of doubling
16553    /// size (1, 2, 4, ... vertices, capped so a slice does not overshoot the
16554    /// probe) and hands the rest to the pool once the rest looks worth it
16555    /// (`PARALLEL_LAYER_WORTH` at the rate so far) or the probe
16556    /// (`PARALLEL_LAYER_PROBE`) is spent. A cheap layer never pays the pool's
16557    /// wake-up and join (most Enron layers are tens of µs of work); an
16558    /// expensive one goes parallel after a few vertices. Splitting a layer
16559    /// into consecutive sub-layers is a valid order: its vertices are
16560    /// independent.
16561    fn evaluate_layer_parallel(
16562        &mut self,
16563        layer: &super::scheduler::Layer,
16564    ) -> Result<usize, ExcelError> {
16565        if layer.sequential {
16566            return self.evaluate_layer_sequential(layer);
16567        }
16568        self.resource_checkpoint(layer.vertices.len() as u64)?;
16569        let len = layer.vertices.len();
16570        let buffered = buffer_layer_writes(layer);
16571        let (probe, worth) = (PARALLEL_LAYER_PROBE, PARALLEL_LAYER_WORTH);
16572        let start = crate::instant::FzInstant::now();
16573        let mut pos = 0usize;
16574        let mut step = 1usize;
16575        while pos < len {
16576            if pos > 0 {
16577                let elapsed = start.elapsed();
16578                // Rate so far (ns per vertex) and the rest at that rate.
16579                let per_vertex = elapsed.as_nanos() / pos as u128 + 1;
16580                let rest_estimate = per_vertex * (len - pos) as u128;
16581                if len - pos >= 2 && (elapsed >= probe || rest_estimate >= worth.as_nanos()) {
16582                    let rest = layer.sub_layer(pos, len);
16583                    // Expensive members (a SUMIF over a table) parallelize
16584                    // one per task; cheap ones keep runs of 8 together.
16585                    let min_chunk = if per_vertex >= EXPENSIVE_VERTEX_NS {
16586                        1
16587                    } else {
16588                        8
16589                    };
16590                    return Ok(pos + self.evaluate_layer_parallel_effects(&rest, min_chunk)?);
16591                }
16592                // Next slice: double, but no more than the rest of the probe
16593                // at the rate so far (a slice must not overshoot it).
16594                let fit = (probe.saturating_sub(elapsed).as_nanos() / per_vertex) as usize + 1;
16595                step = step.saturating_mul(2).min(fit);
16596            }
16597            let end = (pos + step).min(len);
16598            let slice = layer.sub_layer(pos, end);
16599            // A slice stops at the probe's end even if its members turn out
16600            // far more expensive than the rate so far predicted.
16601            pos += self.evaluate_layer_units_until(
16602                &slice,
16603                None,
16604                None,
16605                None,
16606                buffered,
16607                Some(start + probe),
16608            )?;
16609        }
16610        Ok(len)
16611    }
16612
16613    fn evaluate_layer_parallel_with_delta(
16614        &mut self,
16615        layer: &super::scheduler::Layer,
16616        delta: &mut DeltaCollector,
16617    ) -> Result<usize, ExcelError> {
16618        if layer.sequential {
16619            return self.evaluate_layer_sequential_with_delta(layer, delta);
16620        }
16621        self.resource_checkpoint(layer.vertices.len() as u64)?;
16622        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
16623    }
16624
16625    /// Evaluate a layer in parallel with cancellation support
16626    fn evaluate_layer_parallel_cancellable(
16627        &mut self,
16628        layer: &super::scheduler::Layer,
16629        cancel_flag: &AtomicBool,
16630    ) -> Result<usize, ExcelError> {
16631        if layer.sequential {
16632            return self.evaluate_layer_sequential_cancellable(layer, cancel_flag);
16633        }
16634        self.resource_checkpoint(layer.vertices.len() as u64)?;
16635        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
16636    }
16637
16638    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
16639    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
16640        // Check if vertex exists
16641        if !self.graph.vertex_exists(vertex_id) {
16642            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
16643                .with_message(format!("Vertex not found: {vertex_id:?}")));
16644        }
16645
16646        // Get vertex kind and check if it needs evaluation
16647        let kind = self.graph.get_vertex_kind(vertex_id);
16648        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
16649
16650        let view = match kind {
16651            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
16652                if let Some(view) = self.graph.formula_view(vertex_id) {
16653                    view
16654                } else {
16655                    return Ok(LiteralValue::Number(0.0));
16656                }
16657            }
16658            VertexKind::Empty | VertexKind::Cell => {
16659                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
16660                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16661                    let row = cell_ref.coord.row() + 1;
16662                    let col = cell_ref.coord.col() + 1;
16663                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
16664                        return Ok(v);
16665                    }
16666                }
16667                return Ok(LiteralValue::Number(0.0));
16668            }
16669            VertexKind::NamedScalar => {
16670                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
16671                    ExcelError::new(ExcelErrorKind::Name)
16672                        .with_message("Named range metadata missing".to_string())
16673                })?;
16674
16675                return match &named_range.definition {
16676                    NamedDefinition::Cell(cell_ref) => {
16677                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16678                        Ok(self
16679                            .get_cell_value(
16680                                sheet_name,
16681                                cell_ref.coord.row() + 1,
16682                                cell_ref.coord.col() + 1,
16683                            )
16684                            .unwrap_or(LiteralValue::Empty))
16685                    }
16686                    NamedDefinition::Literal(v) => Ok(v.clone()),
16687                    NamedDefinition::Formula { ast, .. } => {
16688                        let context_sheet = match named_range.scope {
16689                            NameScope::Sheet(id) => id,
16690                            NameScope::Workbook => sheet_id,
16691                        };
16692                        let sheet_name = self.graph.sheet_name(context_sheet);
16693                        let cell_ref = self
16694                            .graph
16695                            .get_cell_ref(vertex_id)
16696                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
16697                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16698                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
16699                    }
16700                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
16701                        .with_message("Range-valued name evaluated as scalar".to_string())),
16702                };
16703            }
16704            VertexKind::NamedArray => {
16705                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
16706                    ExcelError::new(ExcelErrorKind::Name)
16707                        .with_message("Named range metadata missing".to_string())
16708                })?;
16709
16710                return match &named_range.definition {
16711                    NamedDefinition::Range(range_ref) => {
16712                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
16713                            return Err(ExcelError::new(ExcelErrorKind::Ref)
16714                                .with_message("Named range cannot span sheets".to_string()));
16715                        }
16716                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
16717                        let sr0 = range_ref.start.coord.row();
16718                        let sc0 = range_ref.start.coord.col();
16719                        let er0 = range_ref.end.coord.row();
16720                        let ec0 = range_ref.end.coord.col();
16721                        if sr0 > er0 || sc0 > ec0 {
16722                            return Err(ExcelError::new(ExcelErrorKind::Ref)
16723                                .with_message("Invalid named range bounds".to_string()));
16724                        }
16725
16726                        let h = (er0 - sr0 + 1) as usize;
16727                        let w = (ec0 - sc0 + 1) as usize;
16728                        let cell_count = (h as u64).saturating_mul(w as u64);
16729                        if cell_count > self.config.spill.max_spill_cells as u64 {
16730                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
16731                                "Named range too large to materialize as an array".to_string(),
16732                            ));
16733                        }
16734
16735                        // `get_cell_value` per cell, with the sheet resolved
16736                        // once (`read_cell_formatted_in` is its body).
16737                        let sheet_id = range_ref.start.sheet_id;
16738                        let asheet = self.arrow_sheets.sheet(sheet_name);
16739                        let mut rows = Vec::with_capacity(h);
16740                        for r0 in sr0..=er0 {
16741                            let mut row = Vec::with_capacity(w);
16742                            for c0 in sc0..=ec0 {
16743                                row.push(
16744                                    self.read_cell_formatted_in(sheet_id, asheet, r0 + 1, c0 + 1)
16745                                        .0,
16746                                );
16747                            }
16748                            rows.push(row);
16749                        }
16750                        Ok(LiteralValue::Array(rows))
16751                    }
16752                    NamedDefinition::Cell(cell_ref) => {
16753                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
16754                        let row = cell_ref.coord.row() + 1;
16755                        let col = cell_ref.coord.col() + 1;
16756                        let v = self
16757                            .get_cell_value(sheet_name, row, col)
16758                            .unwrap_or(LiteralValue::Empty);
16759                        Ok(LiteralValue::Array(vec![vec![v]]))
16760                    }
16761                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
16762                    NamedDefinition::Formula { ast, .. } => {
16763                        let context_sheet = match named_range.scope {
16764                            NameScope::Sheet(id) => id,
16765                            NameScope::Workbook => sheet_id,
16766                        };
16767                        let sheet_name = self.graph.sheet_name(context_sheet);
16768                        let cell_ref = self
16769                            .graph
16770                            .get_cell_ref(vertex_id)
16771                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
16772                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16773                        match interpreter.evaluate_ast(ast) {
16774                            Ok(cv) => {
16775                                let v = cv.into_literal();
16776                                match v {
16777                                    LiteralValue::Array(_) => Ok(v),
16778                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
16779                                }
16780                            }
16781                            Err(err) => Ok(LiteralValue::Error(err)),
16782                        }
16783                    }
16784                };
16785            }
16786            VertexKind::InfiniteRange
16787            | VertexKind::Range
16788            | VertexKind::External
16789            | VertexKind::Table => {
16790                // Not directly evaluatable here.
16791                return Ok(LiteralValue::Number(0.0));
16792            }
16793        };
16794
16795        // The interpreter uses a reference to the engine as the context
16796        let sheet_name = self.graph.sheet_name(sheet_id);
16797        let cell_ref = self
16798            .graph
16799            .get_cell_ref(vertex_id)
16800            .expect("cell ref for vertex");
16801        if let Some(result) =
16802            self.freshness_evaluate_recorded(vertex_id, sheet_name, cell_ref, view)
16803        {
16804            return result;
16805        }
16806        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
16807
16808        interpreter
16809            .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
16810            .map(|cv| {
16811                let format = cv.format_id();
16812                self.record_derived_format(vertex_id, format);
16813                crate::engine::result_finalization::finalize_published_calc_result(
16814                    cv,
16815                    self.config.spill.max_spill_cells,
16816                )
16817            })
16818    }
16819
16820    /// Get access to the shared thread pool for parallel evaluation
16821    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
16822        self.thread_pool.as_ref()
16823    }
16824}
16825
16826#[derive(Default)]
16827struct RowBoundsCache {
16828    snapshot: u64,
16829    // key: (sheet_id, col_idx)
16830    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
16831}
16832
16833impl RowBoundsCache {
16834    fn new(snapshot: u64) -> Self {
16835        Self {
16836            snapshot,
16837            map: Default::default(),
16838        }
16839    }
16840    fn get_row_bounds(
16841        &self,
16842        sheet_id: SheetId,
16843        col_idx: usize,
16844        snapshot: u64,
16845    ) -> Option<(Option<u32>, Option<u32>)> {
16846        if self.snapshot != snapshot {
16847            return None;
16848        }
16849        self.map.get(&(sheet_id as u32, col_idx)).copied()
16850    }
16851    fn put_row_bounds(
16852        &mut self,
16853        sheet_id: SheetId,
16854        col_idx: usize,
16855        snapshot: u64,
16856        bounds: (Option<u32>, Option<u32>),
16857    ) {
16858        if self.snapshot != snapshot {
16859            self.snapshot = snapshot;
16860            self.map.clear();
16861        }
16862        self.map.insert((sheet_id as u32, col_idx), bounds);
16863    }
16864}
16865
16866struct UsedAxisBoundsCache {
16867    snapshot: u64,
16868    row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
16869    col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
16870    #[cfg(test)]
16871    row_hits: std::sync::atomic::AtomicUsize,
16872    #[cfg(test)]
16873    row_misses: std::sync::atomic::AtomicUsize,
16874    #[cfg(test)]
16875    col_hits: std::sync::atomic::AtomicUsize,
16876    #[cfg(test)]
16877    col_misses: std::sync::atomic::AtomicUsize,
16878}
16879
16880impl UsedAxisBoundsCache {
16881    fn new(snapshot: u64) -> Self {
16882        Self {
16883            snapshot,
16884            row_bounds_by_col_span: Default::default(),
16885            col_bounds_by_row_span: Default::default(),
16886            #[cfg(test)]
16887            row_hits: std::sync::atomic::AtomicUsize::new(0),
16888            #[cfg(test)]
16889            row_misses: std::sync::atomic::AtomicUsize::new(0),
16890            #[cfg(test)]
16891            col_hits: std::sync::atomic::AtomicUsize::new(0),
16892            #[cfg(test)]
16893            col_misses: std::sync::atomic::AtomicUsize::new(0),
16894        }
16895    }
16896
16897    fn reset_for_snapshot(&mut self, snapshot: u64) {
16898        if self.snapshot != snapshot {
16899            self.snapshot = snapshot;
16900            self.row_bounds_by_col_span.clear();
16901            self.col_bounds_by_row_span.clear();
16902        }
16903    }
16904
16905    fn get_row_bounds(
16906        &self,
16907        sheet_id: SheetId,
16908        start_col: u32,
16909        end_col: u32,
16910        snapshot: u64,
16911    ) -> Option<Option<(u32, u32)>> {
16912        if self.snapshot != snapshot {
16913            return None;
16914        }
16915        let cached = self
16916            .row_bounds_by_col_span
16917            .get(&(sheet_id, start_col, end_col))
16918            .copied();
16919        #[cfg(test)]
16920        if cached.is_some() {
16921            self.row_hits.fetch_add(1, Ordering::Relaxed);
16922        }
16923        cached
16924    }
16925
16926    fn put_row_bounds(
16927        &mut self,
16928        sheet_id: SheetId,
16929        start_col: u32,
16930        end_col: u32,
16931        snapshot: u64,
16932        bounds: Option<(u32, u32)>,
16933    ) {
16934        self.reset_for_snapshot(snapshot);
16935        self.row_bounds_by_col_span
16936            .insert((sheet_id, start_col, end_col), bounds);
16937        #[cfg(test)]
16938        self.row_misses.fetch_add(1, Ordering::Relaxed);
16939    }
16940
16941    fn get_col_bounds(
16942        &self,
16943        sheet_id: SheetId,
16944        start_row: u32,
16945        end_row: u32,
16946        snapshot: u64,
16947    ) -> Option<Option<(u32, u32)>> {
16948        if self.snapshot != snapshot {
16949            return None;
16950        }
16951        let cached = self
16952            .col_bounds_by_row_span
16953            .get(&(sheet_id, start_row, end_row))
16954            .copied();
16955        #[cfg(test)]
16956        if cached.is_some() {
16957            self.col_hits.fetch_add(1, Ordering::Relaxed);
16958        }
16959        cached
16960    }
16961
16962    fn put_col_bounds(
16963        &mut self,
16964        sheet_id: SheetId,
16965        start_row: u32,
16966        end_row: u32,
16967        snapshot: u64,
16968        bounds: Option<(u32, u32)>,
16969    ) {
16970        self.reset_for_snapshot(snapshot);
16971        self.col_bounds_by_row_span
16972            .insert((sheet_id, start_row, end_row), bounds);
16973        #[cfg(test)]
16974        self.col_misses.fetch_add(1, Ordering::Relaxed);
16975    }
16976}
16977
16978// Phase 2 shim: in-process spill manager delegating to current graph methods.
16979#[derive(Default)]
16980pub struct ShimSpillManager {
16981    region_locks: RegionLockManager,
16982    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
16983}
16984
16985impl ShimSpillManager {
16986    pub(crate) fn reserve(
16987        &mut self,
16988        owner: VertexId,
16989        anchor_cell: CellRef,
16990        shape: SpillShape,
16991        _meta: SpillMeta,
16992    ) -> Result<(), ExcelError> {
16993        // Derive region from anchor + shape; enforce in-flight exclusivity only.
16994        let region = crate::engine::spill::Region {
16995            sheet_id: anchor_cell.sheet_id as u32,
16996            row_start: anchor_cell.coord.row(),
16997            row_end: anchor_cell
16998                .coord
16999                .row()
17000                .saturating_add(shape.rows)
17001                .saturating_sub(1),
17002            col_start: anchor_cell.coord.col(),
17003            col_end: anchor_cell
17004                .coord
17005                .col()
17006                .saturating_add(shape.cols)
17007                .saturating_sub(1),
17008        };
17009        match self.region_locks.reserve(region, owner) {
17010            Ok(id) => {
17011                if id != 0 {
17012                    self.active_locks.insert(owner, id);
17013                }
17014                Ok(())
17015            }
17016            Err(e) => Err(e),
17017        }
17018    }
17019
17020    /// Release any in-flight region reservation still held for `owner`.
17021    ///
17022    /// Reservations are normally released on commit/rollback, but if an anchor is
17023    /// abandoned without committing (e.g. cycle detection stamps it with #CIRC), a
17024    /// stale reservation could remain. This is a no-op when nothing is held.
17025    pub(crate) fn release_owner(&mut self, owner: VertexId) {
17026        if let Some(id) = self.active_locks.remove(&owner) {
17027            self.region_locks.release(id);
17028        }
17029    }
17030
17031    pub(crate) fn commit_array_with_value_probe<F>(
17032        &mut self,
17033        graph: &mut DependencyGraph,
17034        anchor_vertex: VertexId,
17035        targets: &[CellRef],
17036        rows: Vec<Vec<LiteralValue>>,
17037        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
17038        mut value_probe: F,
17039    ) -> Result<(), ExcelError>
17040    where
17041        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
17042    {
17043        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
17044
17045        // Re-run plan on concrete targets before committing to respect blockers.
17046        // This plan checks formula/spill ownership in the graph, but when the graph value cache
17047        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
17048        let plan_res = graph.plan_spill_region_allowing_formula_overwrite(
17049            anchor_vertex,
17050            targets,
17051            overwritable_formulas,
17052        );
17053        if let Err(e) = plan_res {
17054            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17055                self.region_locks.release(id);
17056            }
17057            return Err(e);
17058        }
17059
17060        if !graph.value_cache_enabled() {
17061            // Compute expected spill shape from the target rectangle for diagnostics.
17062            let (expected_rows, expected_cols) = if targets.is_empty() {
17063                (0u32, 0u32)
17064            } else {
17065                let mut min_r = u32::MAX;
17066                let mut max_r = 0u32;
17067                let mut min_c = u32::MAX;
17068                let mut max_c = 0u32;
17069                for cell in targets {
17070                    let r = cell.coord.row();
17071                    let c = cell.coord.col();
17072                    min_r = min_r.min(r);
17073                    max_r = max_r.max(r);
17074                    min_c = min_c.min(c);
17075                    max_c = max_c.max(c);
17076                }
17077                (
17078                    max_r.saturating_sub(min_r).saturating_add(1),
17079                    max_c.saturating_sub(min_c).saturating_add(1),
17080                )
17081            };
17082
17083            let anchor_cell = graph
17084                .get_cell_ref(anchor_vertex)
17085                .expect("anchor cell ref for spill commit");
17086
17087            for cell in targets {
17088                // Never treat the anchor as a blocker.
17089                if *cell == anchor_cell {
17090                    continue;
17091                }
17092                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
17093                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
17094                    continue;
17095                }
17096                // Skip formula vertices in the target region; plan() handled them (or allowed).
17097                if let Some(vid) = graph.get_vertex_id_for_address(cell)
17098                    && vid != anchor_vertex
17099                {
17100                    match graph.get_vertex_kind(vid) {
17101                        crate::engine::vertex::VertexKind::FormulaScalar
17102                        | crate::engine::vertex::VertexKind::FormulaArray => {
17103                            // plan() already approved allowed overwrites.
17104                            continue;
17105                        }
17106                        _ => {}
17107                    }
17108                }
17109
17110                if let Some(v) = value_probe(graph, cell)
17111                    && !matches!(v, LiteralValue::Empty)
17112                {
17113                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17114                        self.region_locks.release(id);
17115                    }
17116                    return Err(ExcelError::new(ExcelErrorKind::Spill)
17117                        .with_message("BlockedByValue")
17118                        .with_extra(ExcelErrorExtra::Spill {
17119                            expected_rows,
17120                            expected_cols,
17121                        }));
17122                }
17123            }
17124        }
17125
17126        let commit_res = graph.commit_spill_region_atomic_with_fault(
17127            anchor_vertex,
17128            targets.to_vec(),
17129            rows,
17130            None,
17131        );
17132        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17133            self.region_locks.release(id);
17134        }
17135        commit_res.map(|_| ())
17136    }
17137
17138    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
17139    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
17140        &mut self,
17141        engine: &mut Engine<R>,
17142        anchor_vertex: VertexId,
17143        targets: &[CellRef],
17144        rows: Vec<Vec<LiteralValue>>,
17145        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
17146    ) -> Result<(), ExcelError> {
17147        if let Err(error) = engine.guard_pending_spill_commit(anchor_vertex, targets) {
17148            self.release_owner(anchor_vertex);
17149            return Err(error);
17150        }
17151        // Re-run plan on concrete targets before committing to respect blockers.
17152        let plan_res = engine.graph.plan_spill_region_allowing_formula_overwrite(
17153            anchor_vertex,
17154            targets,
17155            overwritable_formulas,
17156        );
17157        if let Err(e) = plan_res {
17158            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17159                self.region_locks.release(id);
17160            }
17161            return Err(e);
17162        }
17163
17164        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
17165            anchor_vertex,
17166            targets.to_vec(),
17167            rows.clone(),
17168            None,
17169        );
17170        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17171            self.region_locks.release(id);
17172        }
17173        commit_res.map(|_| ())?;
17174        engine
17175            .blocked_pending_spills
17176            .retain(|entry| entry.0 != anchor_vertex);
17177
17178        // Mirror into Arrow overlay when enabled
17179        if engine.config.arrow_storage_enabled
17180            && engine.config.delta_overlay_enabled
17181            && engine.config.write_formula_overlay_enabled
17182        {
17183            // Expect targets to be a contiguous rectangle row-major starting at some anchor
17184            for (idx, cell) in targets.iter().enumerate() {
17185                let (r_off, c_off) = {
17186                    if rows.is_empty() || rows[0].is_empty() {
17187                        (0usize, 0usize)
17188                    } else {
17189                        let width = rows[0].len();
17190                        (idx / width, idx % width)
17191                    }
17192                };
17193                let v = rows
17194                    .get(r_off)
17195                    .and_then(|r| r.get(c_off))
17196                    .cloned()
17197                    .unwrap_or(LiteralValue::Empty);
17198                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
17199                engine.mirror_value_to_computed_overlay(
17200                    &sheet_name,
17201                    cell.coord.row() + 1,
17202                    cell.coord.col() + 1,
17203                    &v,
17204                );
17205            }
17206        }
17207        Ok(())
17208    }
17209}
17210
17211impl<R> Engine<R>
17212where
17213    R: EvaluationContext,
17214{
17215    fn resolve_shared_ref(
17216        &self,
17217        reference: &ReferenceType,
17218        current_sheet: &str,
17219    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
17220        use formualizer_common::{
17221            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
17222            SheetRef as SharedRef,
17223        };
17224
17225        // Preserve anchor flags from the parsed reference when possible.
17226        let sr = match reference {
17227            ReferenceType::Cell {
17228                sheet,
17229                row,
17230                col,
17231                row_abs,
17232                col_abs,
17233            } => {
17234                let row0 = row
17235                    .checked_sub(1)
17236                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17237                let col0 = col
17238                    .checked_sub(1)
17239                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17240                let sheet_loc = match sheet.as_deref() {
17241                    Some(name) => SheetLocator::from_name(name),
17242                    None => SheetLocator::Current,
17243                };
17244                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
17245                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
17246            }
17247            ReferenceType::Range {
17248                sheet,
17249                start_row,
17250                start_col,
17251                end_row,
17252                end_col,
17253                start_row_abs,
17254                start_col_abs,
17255                end_row_abs,
17256                end_col_abs,
17257            } => {
17258                let sheet_loc = match sheet.as_deref() {
17259                    Some(name) => SheetLocator::from_name(name),
17260                    None => SheetLocator::Current,
17261                };
17262                let sr = start_row
17263                    .map(|r| {
17264                        r.checked_sub(1)
17265                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17266                    })
17267                    .transpose()?;
17268                let sc = start_col
17269                    .map(|c| {
17270                        c.checked_sub(1)
17271                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17272                    })
17273                    .transpose()?;
17274                let er = end_row
17275                    .map(|r| {
17276                        r.checked_sub(1)
17277                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17278                    })
17279                    .transpose()?;
17280                let ec = end_col
17281                    .map(|c| {
17282                        c.checked_sub(1)
17283                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17284                    })
17285                    .transpose()?;
17286                let range = SharedRangeRef::from_parts(
17287                    sheet_loc,
17288                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
17289                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
17290                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
17291                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
17292                )
17293                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
17294                SharedRef::Range(range)
17295            }
17296            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
17297        };
17298
17299        let current_id = self
17300            .graph
17301            .sheet_id(current_sheet)
17302            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17303
17304        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
17305            match loc {
17306                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
17307                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
17308                SheetLocator::Name(name) => {
17309                    let n = name.as_ref();
17310                    self.graph
17311                        .sheet_id(n)
17312                        .map(SheetLocator::Id)
17313                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17314                }
17315            }
17316        };
17317
17318        match sr {
17319            SharedRef::Cell(cell) => {
17320                let owned = cell.into_owned();
17321                let sheet = resolve_loc(owned.sheet)?;
17322                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
17323            }
17324            SharedRef::Range(range) => {
17325                let owned = range.into_owned();
17326                let sheet = resolve_loc(owned.sheet)?;
17327                Ok(SharedRef::Range(SharedRangeRef {
17328                    sheet,
17329                    start_row: owned.start_row,
17330                    start_col: owned.start_col,
17331                    end_row: owned.end_row,
17332                    end_col: owned.end_col,
17333                }))
17334            }
17335        }
17336    }
17337}
17338
17339// Implement the resolver traits for the Engine.
17340// This allows the interpreter to resolve references by querying the engine's graph.
17341impl<R> crate::traits::ReferenceResolver for Engine<R>
17342where
17343    R: EvaluationContext,
17344{
17345    fn resolve_cell_reference(
17346        &self,
17347        sheet: Option<&str>,
17348        row: u32,
17349        col: u32,
17350    ) -> Result<LiteralValue, ExcelError> {
17351        // This context-free trait method has no knowledge of the formula's
17352        // current sheet, so an unqualified (`None`) reference cannot be resolved
17353        // here. Previously this fell back to `default_sheet_name()`, which leaked
17354        // the reference onto an unrelated sheet (issue #110). Interpreter paths
17355        // already qualify references with the current sheet before reaching this
17356        // method (see `Interpreter::implicit_intersection_from_reference`), and
17357        // the sheet-aware scalar path goes through `resolve_cell_reference_value`
17358        // with an explicit `current_sheet`. Returning #REF! for an unqualified
17359        // reference here surfaces the missing context instead of silently
17360        // returning data from the wrong sheet.
17361        let Some(sheet_name) = sheet else {
17362            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
17363                "Unqualified cell reference resolved without sheet context".to_string(),
17364            ));
17365        };
17366        // Prefer engine's unified accessor which consults Arrow store for base values
17367        // and falls back to graph for formulas and stored values.
17368        if let Some(v) = self.get_cell_value(sheet_name, row, col) {
17369            Ok(v)
17370        } else {
17371            // Excel semantics: empty cell coerces to 0 in numeric contexts
17372            Ok(LiteralValue::Number(0.0))
17373        }
17374    }
17375}
17376
17377impl<R> crate::traits::RangeResolver for Engine<R>
17378where
17379    R: EvaluationContext,
17380{
17381    fn resolve_range_reference(
17382        &self,
17383        sheet: Option<&str>,
17384        sr: Option<u32>,
17385        sc: Option<u32>,
17386        er: Option<u32>,
17387        ec: Option<u32>,
17388    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
17389        // For now, delegate range resolution to the external resolver.
17390        // A future optimization could be to handle this within the graph.
17391        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
17392    }
17393}
17394
17395impl<R> crate::traits::NamedRangeResolver for Engine<R>
17396where
17397    R: EvaluationContext,
17398{
17399    fn resolve_named_range_reference(
17400        &self,
17401        name: &str,
17402    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
17403        self.resolver.resolve_named_range_reference(name)
17404    }
17405}
17406
17407impl<R> crate::traits::TableResolver for Engine<R>
17408where
17409    R: EvaluationContext,
17410{
17411    fn resolve_table_reference(
17412        &self,
17413        tref: &formualizer_parse::parser::TableReference,
17414    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17415        self.resolver.resolve_table_reference(tref)
17416    }
17417}
17418
17419impl<R> crate::traits::SourceResolver for Engine<R>
17420where
17421    R: EvaluationContext,
17422{
17423    fn source_scalar_version(&self, name: &str) -> Option<u64> {
17424        self.resolver.source_scalar_version(name)
17425    }
17426
17427    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
17428        self.resolver.resolve_source_scalar(name)
17429    }
17430
17431    fn source_table_version(&self, name: &str) -> Option<u64> {
17432        self.resolver.source_table_version(name)
17433    }
17434
17435    fn resolve_source_table(
17436        &self,
17437        name: &str,
17438    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17439        self.resolver.resolve_source_table(name)
17440    }
17441}
17442
17443// The Engine is a Resolver because it implements the constituent traits.
17444impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
17445
17446// The Engine provides functions by delegating to its internal resolver.
17447impl<R> crate::traits::FunctionProvider for Engine<R>
17448where
17449    R: EvaluationContext,
17450{
17451    fn planning_semantic_revision(&self) -> Option<u64> {
17452        self.resolver.planning_semantic_revision()
17453    }
17454
17455    fn get_function(
17456        &self,
17457        prefix: &str,
17458        name: &str,
17459    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17460        self.resolver.get_function(prefix, name)
17461    }
17462
17463    fn get_function_for_planning(
17464        &self,
17465        prefix: &str,
17466        name: &str,
17467    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17468        self.resolver.get_function_for_planning(prefix, name)
17469    }
17470}
17471
17472impl<R> Engine<R>
17473where
17474    R: EvaluationContext,
17475{
17476    /// Semantic used coordinates exclude graph-only dependency placeholders.
17477    ///
17478    /// Non-empty base/overlay/computed cells come from Arrow storage, while
17479    /// scalar and array formulas come from graph formula kinds even before
17480    /// their results are materialized. The legacy graph fallback is omitted:
17481    /// `load_packed_to_vertex` entries are either represented by those sources
17482    /// or are `Empty` dependency placeholders, not a third value authority.
17483    pub(crate) fn semantic_used_rows_for_columns(
17484        &self,
17485        sheet: &str,
17486        start_col: u32,
17487        end_col: u32,
17488    ) -> Option<(u32, u32)> {
17489        let arrow_bounds = self
17490            .sheet_store()
17491            .sheet(sheet)
17492            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
17493        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
17494        Self::union_used_bounds(arrow_bounds, formula_bounds)
17495    }
17496
17497    pub(crate) fn semantic_used_cols_for_rows(
17498        &self,
17499        sheet: &str,
17500        start_row: u32,
17501        end_row: u32,
17502    ) -> Option<(u32, u32)> {
17503        let arrow_bounds = self
17504            .sheet_store()
17505            .sheet(sheet)
17506            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
17507        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
17508        Self::union_used_bounds(arrow_bounds, formula_bounds)
17509    }
17510}
17511
17512// Override EvaluationContext to provide thread pool access
17513impl<R> crate::traits::EvaluationContext for Engine<R>
17514where
17515    R: EvaluationContext,
17516{
17517    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
17518        &self.clock
17519    }
17520
17521    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
17522        self.thread_pool.as_ref()
17523    }
17524
17525    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
17526        self.active_cancel_flag.clone()
17527    }
17528
17529    fn chunk_hint(&self) -> Option<usize> {
17530        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
17531        let hint =
17532            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
17533        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
17534    }
17535
17536    fn volatile_level(&self) -> crate::traits::VolatileLevel {
17537        self.config.volatile_level
17538    }
17539
17540    fn workbook_seed(&self) -> u64 {
17541        self.config.workbook_seed
17542    }
17543
17544    fn recalc_epoch(&self) -> u64 {
17545        self.recalc_epoch
17546    }
17547
17548    fn workbook_sheet_count(&self) -> Option<usize> {
17549        Some(self.graph.sheet_reg().active_len())
17550    }
17551
17552    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
17553        self.graph.sheet_reg().active_position(sheet)
17554    }
17555
17556    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
17557        self.sheet_index_by_name(current_sheet)
17558    }
17559
17560    fn inspect_reference(
17561        &self,
17562        reference: &ReferenceType,
17563        current_sheet: &str,
17564    ) -> Result<Option<ReferenceInfo>, ExcelError> {
17565        let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
17566            let sheet_id = self
17567                .graph
17568                .sheet_id(sheet_name)
17569                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17570            let sheet_index = self
17571                .graph
17572                .sheet_reg()
17573                .active_position_by_id(sheet_id)
17574                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17575            Ok((sheet_id, sheet_index))
17576        };
17577
17578        let cell_info =
17579            |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
17580                let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
17581                let row0 = row
17582                    .checked_sub(1)
17583                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17584                let col0 = col
17585                    .checked_sub(1)
17586                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17587                Ok(ReferenceInfo {
17588                    first_sheet_index: Some(sheet_index),
17589                    sheet_count: Some(1),
17590                    first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
17591                })
17592            };
17593
17594        let range_info = |sheet_name: &str,
17595                          start_row: Option<u32>,
17596                          start_col: Option<u32>|
17597         -> Result<ReferenceInfo, ExcelError> {
17598            let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
17599            let row = start_row.unwrap_or(1);
17600            let col = start_col.unwrap_or(1);
17601            let row0 = row
17602                .checked_sub(1)
17603                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17604            let col0 = col
17605                .checked_sub(1)
17606                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17607            Ok(ReferenceInfo {
17608                first_sheet_index: Some(sheet_index),
17609                sheet_count: Some(1),
17610                first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
17611            })
17612        };
17613
17614        let info = match reference {
17615            ReferenceType::Cell {
17616                sheet, row, col, ..
17617            } => {
17618                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
17619                cell_info(sheet_name, *row, *col)?
17620            }
17621            ReferenceType::Range {
17622                sheet,
17623                start_row,
17624                start_col,
17625                ..
17626            } => {
17627                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
17628                range_info(sheet_name, *start_row, *start_col)?
17629            }
17630            ReferenceType::Cell3D {
17631                sheet_first,
17632                sheet_last,
17633                row,
17634                col,
17635                ..
17636            } => {
17637                let first = cell_info(sheet_first, *row, *col)?;
17638                ReferenceInfo {
17639                    first_sheet_index: first.first_sheet_index,
17640                    sheet_count: self
17641                        .graph
17642                        .sheet_reg()
17643                        .active_span_len(sheet_first, sheet_last),
17644                    first_cell: first.first_cell,
17645                }
17646            }
17647            ReferenceType::Range3D {
17648                sheet_first,
17649                sheet_last,
17650                start_row,
17651                start_col,
17652                ..
17653            } => {
17654                let first = range_info(sheet_first, *start_row, *start_col)?;
17655                ReferenceInfo {
17656                    first_sheet_index: first.first_sheet_index,
17657                    sheet_count: self
17658                        .graph
17659                        .sheet_reg()
17660                        .active_span_len(sheet_first, sheet_last),
17661                    first_cell: first.first_cell,
17662                }
17663            }
17664            ReferenceType::NamedRange(name) => {
17665                let current_id = self
17666                    .graph
17667                    .sheet_id(current_sheet)
17668                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17669                let named = self
17670                    .graph
17671                    .resolve_name_entry(name, current_id)
17672                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17673                match &named.definition {
17674                    NamedDefinition::Cell(cell) => ReferenceInfo {
17675                        first_sheet_index: self
17676                            .graph
17677                            .sheet_reg()
17678                            .active_position_by_id(cell.sheet_id),
17679                        sheet_count: Some(1),
17680                        first_cell: Some(*cell),
17681                    },
17682                    NamedDefinition::Range(range) => ReferenceInfo {
17683                        first_sheet_index: self
17684                            .graph
17685                            .sheet_reg()
17686                            .active_position_by_id(range.start.sheet_id),
17687                        sheet_count: Some(1),
17688                        first_cell: Some(range.start),
17689                    },
17690                    NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
17691                        ReferenceInfo {
17692                            first_sheet_index: None,
17693                            sheet_count: None,
17694                            first_cell: None,
17695                        }
17696                    }
17697                }
17698            }
17699            ReferenceType::Table(tref) => {
17700                let table = self
17701                    .graph
17702                    .resolve_table_entry(&tref.name)
17703                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17704                ReferenceInfo {
17705                    first_sheet_index: self
17706                        .graph
17707                        .sheet_reg()
17708                        .active_position_by_id(table.range.start.sheet_id),
17709                    sheet_count: Some(1),
17710                    first_cell: Some(table.range.start),
17711                }
17712            }
17713            ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
17714        };
17715
17716        Ok(Some(info))
17717    }
17718
17719    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
17720        let sheet_name = self.graph.sheet_name(cell.sheet_id);
17721        if sheet_name.is_empty() {
17722            return Err(ExcelError::new(ExcelErrorKind::Ref));
17723        }
17724        let row = cell.coord.row() + 1;
17725        let col = cell.coord.col() + 1;
17726
17727        if let Some(entries) = self.staged_formulas.get(sheet_name)
17728            && let Some(text) = entries.get(row, col)
17729        {
17730            return Ok(Some(if text.starts_with('=') {
17731                text.to_owned()
17732            } else {
17733                format!("={text}")
17734            }));
17735        }
17736
17737        let Some((Some(ast), _)) = self.get_cell(sheet_name, row, col) else {
17738            return Ok(None);
17739        };
17740        Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
17741    }
17742
17743    fn used_rows_for_columns(
17744        &self,
17745        sheet: &str,
17746        start_col: u32,
17747        end_col: u32,
17748    ) -> Option<(u32, u32)> {
17749        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
17750        let sheet_id = self.graph.sheet_id(sheet)?;
17751        let snap = self.data_snapshot_id();
17752        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
17753            guard
17754                .as_ref()
17755                .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
17756        }) {
17757            return cached;
17758        }
17759
17760        let arrow_bounds = self
17761            .sheet_store()
17762            .sheet(sheet)
17763            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
17764        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
17765        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
17766            Some(bounds)
17767        } else {
17768            let sc0 = start_col.saturating_sub(1);
17769            let ec0 = end_col.saturating_sub(1);
17770            self.graph
17771                .used_row_bounds_for_columns(sheet_id, sc0, ec0)
17772                .map(|(a0, b0)| (a0 + 1, b0 + 1))
17773        };
17774
17775        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
17776            guard
17777                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
17778                .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
17779        }
17780
17781        computed
17782    }
17783
17784    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
17785        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
17786        let sheet_id = self.graph.sheet_id(sheet)?;
17787        let snap = self.data_snapshot_id();
17788        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
17789            guard
17790                .as_ref()
17791                .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
17792        }) {
17793            return cached;
17794        }
17795
17796        let arrow_bounds = self
17797            .sheet_store()
17798            .sheet(sheet)
17799            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
17800        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
17801        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
17802            Some(bounds)
17803        } else {
17804            let sr0 = start_row.saturating_sub(1);
17805            let er0 = end_row.saturating_sub(1);
17806            self.graph
17807                .used_col_bounds_for_rows(sheet_id, sr0, er0)
17808                .map(|(a0, b0)| (a0 + 1, b0 + 1))
17809        };
17810
17811        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
17812            guard
17813                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
17814                .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
17815        }
17816
17817        computed
17818    }
17819
17820    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
17821        let _ = self.graph.sheet_id(sheet)?;
17822        // Excel-like upper bounds; we expose something finite but large.
17823        // Backends may override with real bounds.
17824        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
17825    }
17826
17827    fn data_snapshot_id(&self) -> u64 {
17828        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
17829    }
17830
17831    fn backend_caps(&self) -> crate::traits::BackendCaps {
17832        crate::traits::BackendCaps {
17833            streaming: true,
17834            used_region: true,
17835            write: false,
17836            tables: false,
17837            async_stream: false,
17838        }
17839    }
17840
17841    fn build_lookup_index(
17842        &self,
17843        view: &RangeView<'_>,
17844        axis: LookupAxis,
17845    ) -> Option<Arc<LookupIndex>> {
17846        self.build_lookup_index_impl(view, axis)
17847    }
17848
17849    // Flats removed
17850
17851    fn date_system(&self) -> crate::engine::DateSystem {
17852        self.config.date_system
17853    }
17854    /// New: resolve a reference into a RangeView (Phase 2 API)
17855    fn resolve_range_view<'c>(
17856        &'c self,
17857        reference: &ReferenceType,
17858        current_sheet: &str,
17859    ) -> Result<RangeView<'c>, ExcelError> {
17860        match reference {
17861            ReferenceType::External(ext) => {
17862                let name = ext.raw.as_str();
17863                match ext.kind {
17864                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
17865                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
17866                            return Err(ExcelError::new(ExcelErrorKind::Name)
17867                                .with_message(format!("Undefined name: {name}")));
17868                        };
17869                        let version = source
17870                            .version
17871                            .or_else(|| self.resolver.source_scalar_version(name));
17872                        let v = self.resolve_source_scalar_cached(name, version)?;
17873                        Ok(RangeView::from_owned_rows(
17874                            vec![vec![v]],
17875                            self.config.date_system,
17876                        ))
17877                    }
17878                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
17879                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
17880                            // A deferred (whole-row/column) external range
17881                            // evaluates to #REF!, as it did before the parser
17882                            // kept it whole (see `unbound_external_range_defers`).
17883                            let kind =
17884                                if crate::engine::refs::unbound_external_range_defers(&ext.kind) {
17885                                    ExcelErrorKind::Ref
17886                                } else {
17887                                    ExcelErrorKind::Name
17888                                };
17889                            return Err(ExcelError::new(kind)
17890                                .with_message(format!("Undefined table: {name}")));
17891                        };
17892                        let version = source
17893                            .version
17894                            .or_else(|| self.resolver.source_table_version(name));
17895                        let table = self.resolve_source_table_cached(name, version)?;
17896                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
17897                        self.source_table_to_range_view(table.as_ref(), &spec)
17898                    }
17899                }
17900            }
17901            ReferenceType::Range { .. } => {
17902                let shared = self.resolve_shared_ref(reference, current_sheet)?;
17903                let formualizer_common::SheetRef::Range(range) = shared else {
17904                    return Err(ExcelError::new(ExcelErrorKind::Ref));
17905                };
17906                // No context sheet is available here, so an unresolved locator
17907                // is #REF! rather than a guess (issue #110).
17908                let sheet_id = match range.sheet {
17909                    formualizer_common::SheetLocator::Id(id) => id,
17910                    formualizer_common::SheetLocator::Current
17911                    | formualizer_common::SheetLocator::Name(_) => {
17912                        return Err(ExcelError::new(ExcelErrorKind::Ref));
17913                    }
17914                };
17915                let sheet_name = self.graph.sheet_name(sheet_id);
17916
17917                let bounded_range = if range.start_row.is_some()
17918                    && range.start_col.is_some()
17919                    && range.end_row.is_some()
17920                    && range.end_col.is_some()
17921                {
17922                    Some(RangeRef::try_from_shared(range.as_ref())?)
17923                } else {
17924                    None
17925                };
17926
17927                let sr = bounded_range
17928                    .as_ref()
17929                    .map(|r| r.start.coord.row() + 1)
17930                    .or_else(|| range.start_row.map(|b| b.index + 1));
17931                let sc = bounded_range
17932                    .as_ref()
17933                    .map(|r| r.start.coord.col() + 1)
17934                    .or_else(|| range.start_col.map(|b| b.index + 1));
17935                let er = bounded_range
17936                    .as_ref()
17937                    .map(|r| r.end.coord.row() + 1)
17938                    .or_else(|| range.end_row.map(|b| b.index + 1));
17939                let ec = bounded_range
17940                    .as_ref()
17941                    .map(|r| r.end.coord.col() + 1)
17942                    .or_else(|| range.end_col.map(|b| b.index + 1));
17943
17944                let extent = resolve_used_extent_with_fallback(
17945                    OpenRangeBounds {
17946                        start_row: sr,
17947                        start_column: sc,
17948                        end_row: er,
17949                        end_column: ec,
17950                    },
17951                    ExtentPolicy::EvaluationCompat {
17952                        fallback_row: None,
17953                        fallback_column: None,
17954                    },
17955                    || {
17956                        self.sheet_bounds(sheet_name)
17957                            .map(|_| self.config.max_open_ended_rows)
17958                    },
17959                    || {
17960                        self.sheet_bounds(sheet_name)
17961                            .map(|_| self.config.max_open_ended_cols)
17962                    },
17963                    |first, last| self.used_rows_for_columns(sheet_name, first, last),
17964                    |first, last| self.used_cols_for_rows(sheet_name, first, last),
17965                );
17966                let (sr, sc, er, ec) = extent
17967                    .map(|extent| {
17968                        (
17969                            extent.start_row,
17970                            extent.start_column,
17971                            extent.end_row,
17972                            extent.end_column,
17973                        )
17974                    })
17975                    .unwrap_or((1, 1, 0, 0));
17976
17977                if self.force_materialize_range_views {
17978                    if er < sr || ec < sc {
17979                        return Ok(RangeView::from_owned_rows(
17980                            Vec::new(),
17981                            self.config.date_system,
17982                        ));
17983                    }
17984                    let h = (er - sr + 1) as u64;
17985                    let w = (ec - sc + 1) as u64;
17986                    let cell_count = h.saturating_mul(w);
17987                    if cell_count <= self.config.spill.max_spill_cells as u64 {
17988                        let mut rows: Vec<Vec<LiteralValue>> = Vec::with_capacity(h as usize);
17989                        for r in sr..=er {
17990                            let mut rowv: Vec<LiteralValue> = Vec::with_capacity(w as usize);
17991                            for c in sc..=ec {
17992                                rowv.push(
17993                                    self.get_cell_value(sheet_name, r, c)
17994                                        .unwrap_or(LiteralValue::Empty),
17995                                );
17996                            }
17997                            rows.push(rowv);
17998                        }
17999                        return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
18000                    }
18001                }
18002
18003                let Some(asheet) = self.sheet_store().sheet(sheet_name) else {
18004                    return Ok(RangeView::from_owned_rows(
18005                        Vec::new(),
18006                        self.config.date_system,
18007                    ));
18008                };
18009
18010                let rv = if er < sr || ec < sc {
18011                    asheet.range_view(1, 1, 0, 0)
18012                } else {
18013                    let sr0 = sr.saturating_sub(1) as usize;
18014                    let sc0 = sc.saturating_sub(1) as usize;
18015                    let er0 = er.saturating_sub(1) as usize;
18016                    let ec0 = ec.saturating_sub(1) as usize;
18017                    asheet.range_view(sr0, sc0, er0, ec0)
18018                };
18019
18020                Ok(rv)
18021            }
18022            ReferenceType::Cell { .. } => {
18023                let shared = self.resolve_shared_ref(reference, current_sheet)?;
18024                let formualizer_common::SheetRef::Cell(cell) = shared else {
18025                    return Err(ExcelError::new(ExcelErrorKind::Ref));
18026                };
18027                let addr = CellRef::try_from_shared(cell)?;
18028                let sheet_id = addr.sheet_id;
18029                let sheet_name = self.graph.sheet_name(sheet_id);
18030                let row = addr.coord.row() + 1;
18031                let col = addr.coord.col() + 1;
18032
18033                if self.force_materialize_range_views {
18034                    let v = self
18035                        .get_cell_value(sheet_name, row, col)
18036                        .unwrap_or(LiteralValue::Empty);
18037                    return Ok(RangeView::from_owned_rows(
18038                        vec![vec![v]],
18039                        self.config.date_system,
18040                    ));
18041                }
18042
18043                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
18044                    let r0 = row.saturating_sub(1) as usize;
18045                    let c0 = col.saturating_sub(1) as usize;
18046                    let rv = asheet.range_view(r0, c0, r0, c0);
18047                    Ok(rv)
18048                } else {
18049                    let v = self
18050                        .get_cell_value(sheet_name, row, col)
18051                        .unwrap_or(LiteralValue::Empty);
18052                    Ok(RangeView::from_owned_rows(
18053                        vec![vec![v]],
18054                        self.config.date_system,
18055                    ))
18056                }
18057            }
18058            ReferenceType::NamedRange(name) => {
18059                if let Some(current_id) = self.graph.sheet_id(current_sheet)
18060                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
18061                {
18062                    match &named.definition {
18063                        NamedDefinition::Cell(cell_ref) => {
18064                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18065                            if self.force_materialize_range_views {
18066                                let v = self
18067                                    .get_cell_value(
18068                                        sheet_name,
18069                                        cell_ref.coord.row() + 1,
18070                                        cell_ref.coord.col() + 1,
18071                                    )
18072                                    .unwrap_or(LiteralValue::Empty);
18073                                return Ok(RangeView::from_owned_rows(
18074                                    vec![vec![v]],
18075                                    self.config.date_system,
18076                                ));
18077                            } else {
18078                                let asheet = self
18079                                    .sheet_store()
18080                                    .sheet(sheet_name)
18081                                    .expect("Arrow sheet missing for named cell");
18082                                let r0 = cell_ref.coord.row() as usize;
18083                                let c0 = cell_ref.coord.col() as usize;
18084                                let rv = asheet.range_view(r0, c0, r0, c0);
18085                                return Ok(rv);
18086                            }
18087                        }
18088                        NamedDefinition::Range(range_ref) => {
18089                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
18090                            let sr = range_ref.start.coord.row() + 1;
18091                            let sc = range_ref.start.coord.col() + 1;
18092                            let er = range_ref.end.coord.row() + 1;
18093                            let ec = range_ref.end.coord.col() + 1;
18094                            if self.force_materialize_range_views {
18095                                let h = (er.saturating_sub(sr) + 1) as u64;
18096                                let w = (ec.saturating_sub(sc) + 1) as u64;
18097                                let cell_count = h.saturating_mul(w);
18098                                if cell_count <= self.config.spill.max_spill_cells as u64 {
18099                                    let mut rows: Vec<Vec<LiteralValue>> =
18100                                        Vec::with_capacity(h as usize);
18101                                    for r in sr..=er {
18102                                        let mut rowv: Vec<LiteralValue> =
18103                                            Vec::with_capacity(w as usize);
18104                                        for c in sc..=ec {
18105                                            rowv.push(
18106                                                self.get_cell_value(sheet_name, r, c)
18107                                                    .unwrap_or(LiteralValue::Empty),
18108                                            );
18109                                        }
18110                                        rows.push(rowv);
18111                                    }
18112                                    return Ok(RangeView::from_owned_rows(
18113                                        rows,
18114                                        self.config.date_system,
18115                                    ));
18116                                }
18117                            }
18118                            let asheet = self
18119                                .sheet_store()
18120                                .sheet(sheet_name)
18121                                .expect("Arrow sheet missing for named range");
18122                            let sr0 = range_ref.start.coord.row() as usize;
18123                            let sc0 = range_ref.start.coord.col() as usize;
18124                            let er0 = range_ref.end.coord.row() as usize;
18125                            let ec0 = range_ref.end.coord.col() as usize;
18126                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
18127                            return Ok(rv);
18128                        }
18129                        NamedDefinition::Literal(v) => {
18130                            return Ok(RangeView::from_owned_rows(
18131                                vec![vec![v.clone()]],
18132                                self.config.date_system,
18133                            ));
18134                        }
18135                        NamedDefinition::Formula { .. } => {
18136                            if let Some(value) = self.graph.get_value(named.vertex) {
18137                                return Ok(RangeView::from_owned_rows(
18138                                    vec![vec![value]],
18139                                    self.config.date_system,
18140                                ));
18141                            }
18142                        }
18143                    }
18144                }
18145
18146                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
18147                    let version = source
18148                        .version
18149                        .or_else(|| self.resolver.source_scalar_version(name));
18150                    let v = self.resolve_source_scalar_cached(name, version)?;
18151                    return Ok(RangeView::from_owned_rows(
18152                        vec![vec![v]],
18153                        self.config.date_system,
18154                    ));
18155                }
18156
18157                let data = self.resolver.resolve_named_range_reference(name)?;
18158                Ok(RangeView::from_owned_rows(data, self.config.date_system))
18159            }
18160            ReferenceType::Table(tref) => {
18161                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
18162                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
18163                    let asheet = self
18164                        .sheet_store()
18165                        .sheet(sheet_name)
18166                        .expect("Arrow sheet missing for table reference");
18167
18168                    let sr0 = table.range.start.coord.row() as usize;
18169                    let sc0 = table.range.start.coord.col() as usize;
18170                    let er0 = table.range.end.coord.row() as usize;
18171                    let ec0 = table.range.end.coord.col() as usize;
18172
18173                    let has_totals = table.totals_row;
18174                    let has_headers = table.header_row;
18175                    let data_sr = if has_headers {
18176                        sr0.saturating_add(1)
18177                    } else {
18178                        sr0
18179                    };
18180                    let data_er = if has_totals {
18181                        er0.saturating_sub(1)
18182                    } else {
18183                        er0
18184                    };
18185
18186                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
18187                        if sr > er || sc > ec {
18188                            asheet.range_view(1, 1, 0, 0)
18189                        } else {
18190                            asheet.range_view(sr, sc, er, ec)
18191                        }
18192                    };
18193
18194                    let av = match &tref.specifier {
18195                        None => {
18196                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18197                                "Table reference without specifier is unsupported".to_string(),
18198                            ));
18199                        }
18200                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
18201                            let Some(idx) = table.col_index(col) else {
18202                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18203                                    "Column refers to unknown table column".to_string(),
18204                                ));
18205                            };
18206                            let c0 = sc0 + idx;
18207                            select(data_sr, c0, data_er, c0)
18208                        }
18209                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
18210                            start,
18211                            end,
18212                        )) => {
18213                            let Some(si) = table.col_index(start) else {
18214                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18215                                    "Column range refers to unknown column(s)".to_string(),
18216                                ));
18217                            };
18218                            let Some(ei) = table.col_index(end) else {
18219                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18220                                    "Column range refers to unknown column(s)".to_string(),
18221                                ));
18222                            };
18223                            let (mut a, mut b) = (si, ei);
18224                            if a > b {
18225                                std::mem::swap(&mut a, &mut b);
18226                            }
18227                            let c_start = sc0 + a;
18228                            let c_end = sc0 + b;
18229                            select(data_sr, c_start, data_er, c_end)
18230                        }
18231                        Some(formualizer_parse::parser::TableSpecifier::All)
18232                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18233                            formualizer_parse::parser::SpecialItem::All,
18234                        )) => select(sr0, sc0, er0, ec0),
18235                        Some(formualizer_parse::parser::TableSpecifier::Data)
18236                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18237                            formualizer_parse::parser::SpecialItem::Data,
18238                        )) => select(data_sr, sc0, data_er, ec0),
18239                        Some(formualizer_parse::parser::TableSpecifier::Headers)
18240                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18241                            formualizer_parse::parser::SpecialItem::Headers,
18242                        )) => {
18243                            if !has_headers {
18244                                asheet.range_view(1, 1, 0, 0)
18245                            } else {
18246                                select(sr0, sc0, sr0, ec0)
18247                            }
18248                        }
18249                        Some(formualizer_parse::parser::TableSpecifier::Totals)
18250                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18251                            formualizer_parse::parser::SpecialItem::Totals,
18252                        )) => {
18253                            if !has_totals {
18254                                asheet.range_view(1, 1, 0, 0)
18255                            } else {
18256                                select(er0, sc0, er0, ec0)
18257                            }
18258                        }
18259                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18260                            formualizer_parse::parser::SpecialItem::ThisRow,
18261                        )) => {
18262                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18263                                "@ (This Row) requires table-aware context; not yet supported"
18264                                    .to_string(),
18265                            ));
18266                        }
18267                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
18268                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
18269                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18270                                "Complex structured references not yet supported".to_string(),
18271                            ));
18272                        }
18273                    };
18274
18275                    return Ok(av);
18276                }
18277
18278                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
18279                    let version = source
18280                        .version
18281                        .or_else(|| self.resolver.source_table_version(&tref.name));
18282                    let table = self.resolve_source_table_cached(&tref.name, version)?;
18283                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
18284                }
18285
18286                // Fallback: materialize via Resolver::resolve_range_like tranche 1.
18287                // A table nobody defines (an unbound reference kept by the
18288                // BestEffort preparation policy) is `#NAME?`, the kind Strict
18289                // reports at preparation, not the resolver's "not implemented".
18290                let boxed = self
18291                    .resolve_range_like(&ReferenceType::Table(tref.clone()))
18292                    .map_err(|e| {
18293                        if e.kind == ExcelErrorKind::NImpl {
18294                            ExcelError::new(ExcelErrorKind::Name)
18295                                .with_message(format!("Unknown table: {}", tref.name))
18296                        } else {
18297                            e
18298                        }
18299                    })?;
18300                let owned = boxed.materialise().into_owned();
18301                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
18302            }
18303            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
18304                Err(ExcelError::new(ExcelErrorKind::NImpl)
18305                    .with_message("3D references are not yet supported".to_string()))
18306            }
18307        }
18308    }
18309
18310    fn resolve_cell_format(
18311        &self,
18312        sheet: Option<&str>,
18313        row: u32,
18314        col: u32,
18315        current_sheet: &str,
18316    ) -> Option<crate::format::FormatId> {
18317        self.effective_format_id(sheet.unwrap_or(current_sheet), row, col)
18318    }
18319
18320    fn format_class(
18321        &self,
18322        format: crate::format::FormatId,
18323    ) -> Option<formualizer_common::numfmt::FormatClass> {
18324        self.format_registry.class(format).cloned()
18325    }
18326
18327    fn record_cell_derived_format(
18328        &self,
18329        sheet: &str,
18330        row: u32,
18331        col: u32,
18332        format: Option<crate::format::FormatId>,
18333    ) {
18334        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
18335            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
18336            self.record_derived_format_at(cell, format);
18337        }
18338    }
18339
18340    fn resolve_cell_reference_value(
18341        &self,
18342        sheet: Option<&str>,
18343        row: u32,
18344        col: u32,
18345        current_sheet: &str,
18346    ) -> Result<LiteralValue, ExcelError> {
18347        let sheet_name = sheet.unwrap_or(current_sheet);
18348        if self.graph.sheet_id(sheet_name).is_none() {
18349            return Err(ExcelError::new(ExcelErrorKind::Ref));
18350        }
18351        Ok(self
18352            .get_cell_value(sheet_name, row, col)
18353            .unwrap_or(LiteralValue::Empty))
18354    }
18355
18356    fn resolve_cell_reference_value_formatted(
18357        &self,
18358        sheet: Option<&str>,
18359        row: u32,
18360        col: u32,
18361        current_sheet: &str,
18362    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
18363        // `resolve_cell_reference_value` + `resolve_cell_format` with one
18364        // sheet lookup of each kind.
18365        let sheet_name = sheet.unwrap_or(current_sheet);
18366        let Some(sheet_id) = self.graph.sheet_id(sheet_name) else {
18367            return Err(ExcelError::new(ExcelErrorKind::Ref));
18368        };
18369        let asheet = self.arrow_sheets.sheet(sheet_name);
18370        Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
18371    }
18372
18373    fn build_criteria_mask(
18374        &self,
18375        view: &RangeView<'_>,
18376        col_in_view: usize,
18377        pred: &crate::args::CriteriaPredicate,
18378    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18379        #[cfg(any(test, feature = "test-support"))]
18380        criteria_mask_test_hooks::note_mask(view.dims().0);
18381        if view.dims().1 == 0 {
18382            return None;
18383        }
18384        // If the view is logically open-ended but the backing sheet has no physical rows,
18385        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
18386        let sheet_rows = view.sheet().nrows as usize;
18387        if sheet_rows == 0 || view.start_row() >= sheet_rows {
18388            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
18389        }
18390        compute_criteria_mask(view, col_in_view, pred)
18391    }
18392
18393    fn build_row_visibility_mask(
18394        &self,
18395        view: &RangeView<'_>,
18396        mode: VisibilityMaskMode,
18397    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18398        self.build_row_visibility_mask_for_view(view, mode)
18399    }
18400}
18401
18402impl<R> Engine<R>
18403where
18404    R: EvaluationContext,
18405{
18406    fn clear_spill_projection_and_mirror(
18407        &mut self,
18408        anchor_vertex: VertexId,
18409        delta: Option<&mut DeltaCollector>,
18410    ) {
18411        let spill_cells = self
18412            .graph
18413            .spill_cells_for_anchor(anchor_vertex)
18414            .map(|cells| cells.to_vec())
18415            .unwrap_or_default();
18416        if spill_cells.is_empty() {
18417            return;
18418        }
18419
18420        if let Some(delta) = delta
18421            && delta.mode != DeltaMode::Off
18422        {
18423            let empty = LiteralValue::Empty;
18424            for cell in spill_cells.iter() {
18425                let sheet_name = self.graph.sheet_name(cell.sheet_id);
18426                let old = self
18427                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18428                    .unwrap_or(LiteralValue::Empty);
18429                if old != empty {
18430                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18431                }
18432            }
18433        }
18434
18435        self.graph.clear_spill_region(anchor_vertex);
18436        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
18437            self.record_structural_change(scope);
18438        }
18439
18440        if self.config.arrow_storage_enabled
18441            && self.config.delta_overlay_enabled
18442            && self.config.write_formula_overlay_enabled
18443        {
18444            let empty = LiteralValue::Empty;
18445            for cell in spill_cells.iter() {
18446                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
18447                self.mirror_value_to_computed_overlay(
18448                    &sheet_name,
18449                    cell.coord.row() + 1,
18450                    cell.coord.col() + 1,
18451                    &empty,
18452                );
18453            }
18454        }
18455    }
18456
18457    /// Apply the evaluation outcome for one cyclic SCC: stamp `#CIRC!` on its
18458    /// (optionally filtered) members via `stamp_cycle_error`.
18459    ///
18460    /// This is the single per-SCC application point used by every schedule
18461    /// consumer walking `Schedule::units` (pre-work for #112, where cyclic
18462    /// SCCs will gain runtime verdicts instead of an unconditional stamp).
18463    ///
18464    /// `dirty_filter` preserves the recalc-plan quirk: when `Some(dirty)`,
18465    /// only members present in the set are stamped.
18466    ///
18467    /// Returns the number of vertices stamped (0 when a filter excludes every
18468    /// member), so callers can keep their site-specific `cycle_errors`
18469    /// accounting.
18470    fn apply_cycle_outcome(
18471        &mut self,
18472        cycle: &[VertexId],
18473        mut delta: Option<&mut DeltaCollector>,
18474        dirty_filter: Option<&FxHashSet<VertexId>>,
18475    ) -> usize {
18476        let circ_error = LiteralValue::Error(
18477            ExcelError::new(ExcelErrorKind::Circ)
18478                .with_message("Circular dependency detected".to_string()),
18479        );
18480        let mut stamped = 0usize;
18481        for &vertex_id in cycle {
18482            if let Some(filter) = dirty_filter
18483                && !filter.contains(&vertex_id)
18484            {
18485                continue;
18486            }
18487            self.stamp_cycle_error(vertex_id, &circ_error, delta.as_deref_mut());
18488            stamped += 1;
18489        }
18490        stamped
18491    }
18492
18493    /// Stamp a vertex with `#CIRC!` as part of cycle handling.
18494    ///
18495    /// Unlike a bare `update_vertex_value`, this first tears down any spill the
18496    /// vertex previously anchored: it clears the spilled cells, releases the graph
18497    /// spill registry, drops any lingering region reservation, and mirrors the
18498    /// cleared cells into the computed overlay — the same teardown a normal scalar/
18499    /// error result performs (see `apply_non_array_result_from_parallel` /
18500    /// `clear_spill_projection_and_mirror`). Without this, a #CIRC stamp on a former
18501    /// spill anchor would leave stale spilled values and a reserved region behind
18502    /// (issue #111).
18503    ///
18504    /// When `delta` is provided, the cleared spill cells are recorded (by
18505    /// `clear_spill_projection_and_mirror`) and the anchor's own #CIRC change is
18506    /// recorded here, matching how other result paths emit deltas.
18507    fn stamp_cycle_error(
18508        &mut self,
18509        vertex_id: VertexId,
18510        circ_error: &LiteralValue,
18511        mut delta: Option<&mut DeltaCollector>,
18512    ) {
18513        // Tear down any previous spill projection/region before overwriting the anchor.
18514        if self.graph.spill_registry_has_anchor(vertex_id) {
18515            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
18516        }
18517        // Drop any reservation that was never committed (defensive; normally released
18518        // on the prior successful commit).
18519        self.spill_mgr.release_owner(vertex_id);
18520
18521        // Record the anchor's own #CIRC delta, like other result paths.
18522        if let Some(d) = delta
18523            && d.mode != DeltaMode::Off
18524            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
18525        {
18526            let sheet_name = self.graph.sheet_name(cell.sheet_id);
18527            let old = self
18528                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18529                .unwrap_or(LiteralValue::Empty);
18530            if old != *circ_error {
18531                d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18532            }
18533        }
18534
18535        self.graph.update_vertex_value_ref(vertex_id, circ_error);
18536        self.mirror_vertex_value_to_overlay(vertex_id, circ_error);
18537    }
18538
18539    /// Dispatch point for one `ScheduleUnit::Cycle` (RFC #112, Stage 2).
18540    ///
18541    /// * `CycleDetection::Static` — today's behavior, byte-for-byte: stamp
18542    ///   `#CIRC!` on the (optionally dirty-filtered) members.
18543    /// * `CycleDetection::Runtime` — evaluate the SCC via
18544    ///   [`Self::evaluate_scc_unit`]. The recalc-plan dirty quirk maps to:
18545    ///   no dirty member → skip the task entirely (values stand); any dirty
18546    ///   member → the whole SCC evaluates (an SCC cannot be partially
18547    ///   evaluated).
18548    ///
18549    /// Returns the number of `#CIRC!`-stamped vertices, so call sites can
18550    /// keep their `cycle_errors` accounting (`> 0` ⇒ count the unit).
18551    fn handle_cycle_unit(
18552        &mut self,
18553        cycle: &[VertexId],
18554        mut delta: Option<&mut DeltaCollector>,
18555        dirty_filter: Option<&FxHashSet<VertexId>>,
18556        cancel_flag: Option<&AtomicBool>,
18557    ) -> Result<usize, ExcelError> {
18558        self.resource_checkpoint(cycle.len() as u64)?;
18559        match self.config.cycle.detection {
18560            CycleDetection::Static => {
18561                Ok(self.apply_cycle_outcome(cycle, delta.as_deref_mut(), dirty_filter))
18562            }
18563            CycleDetection::Runtime => {
18564                if let Some(filter) = dirty_filter
18565                    && !cycle.iter().any(|v| filter.contains(v))
18566                {
18567                    return Ok(0);
18568                }
18569                // Both policies share `evaluate_scc_unit`; they differ only
18570                // in the settle loop's live-cycle arm (Error stamps,
18571                // Iterate keeps passing — RFC #113).
18572                self.evaluate_scc_unit(cycle, delta, cancel_flag)
18573            }
18574        }
18575    }
18576
18577    /// Evaluate one statically-cyclic SCC under `CycleDetection::Runtime`
18578    /// (design doc `formualizer-stage2-scc-evaluation-design.md` §3; contract
18579    /// spec §3; Iterate policy arm per RFC #113).
18580    ///
18581    /// Phantom SCCs (live-acyclic) produce ordinary values under both
18582    /// policies; live cycles get `#CIRC!` with live-cycle-only blast radius
18583    /// under `CyclePolicy::Error`, or Excel-style iterative calculation
18584    /// (converge per spec §6 or cap at `max_iterations` passes) under
18585    /// `CyclePolicy::Iterate`. Runs sequentially on the
18586    /// coordinating thread; commits are write-through per member (no
18587    /// `ComputedWriteBuffer` — that buffer is scoped to layer evaluation and
18588    /// always flushed before a Cycle unit runs, G1), so later members' scalar
18589    /// *and* range reads observe earlier members' results through the overlay
18590    /// cascade. Deltas are recorded once per member at end of task (G11).
18591    ///
18592    /// Returns the number of vertices stamped `#CIRC!`.
18593    ///
18594    /// `pub(crate)` so tests can drive SCC shapes (e.g. name-vertex members)
18595    /// that ingest-time cycle rejection makes unreachable via public edits.
18596    pub(crate) fn evaluate_scc_unit(
18597        &mut self,
18598        cycle: &[VertexId],
18599        mut delta: Option<&mut DeltaCollector>,
18600        cancel_flag: Option<&AtomicBool>,
18601    ) -> Result<usize, ExcelError> {
18602        struct SccMember {
18603            vertex: VertexId,
18604            cell: Option<CellRef>,
18605        }
18606
18607        let task_start = crate::instant::FzInstant::now();
18608
18609        // ── 0. Member order (spec §7.13): cells ascending (sheet, row, col);
18610        // name vertices after, lexicographic by folded canonical name; any
18611        // other vertex kind (defensive — `get_evaluation_vertices` only emits
18612        // formula/name kinds) last by id, never evaluated.
18613        let mut cell_members: Vec<(VertexId, CellRef)> = Vec::new();
18614        let mut name_members: Vec<(VertexId, String)> = Vec::new();
18615        let mut other_members: Vec<VertexId> = Vec::new();
18616        for &v in cycle {
18617            match self.graph.get_vertex_kind(v) {
18618                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18619                    match self.graph.get_cell_ref(v) {
18620                        Some(cell) => cell_members.push((v, cell)),
18621                        None => other_members.push(v),
18622                    }
18623                }
18624                VertexKind::NamedScalar | VertexKind::NamedArray => {
18625                    match self.graph.name_key_for_vertex(v) {
18626                        Some(key) => name_members.push((v, key)),
18627                        None => other_members.push(v),
18628                    }
18629                }
18630                _ => other_members.push(v),
18631            }
18632        }
18633        cell_members.sort_unstable_by_key(|(_, c)| (c.sheet_id, c.coord.row(), c.coord.col()));
18634        name_members.sort_unstable_by(|(av, ak), (bv, bk)| ak.cmp(bk).then(av.cmp(bv)));
18635        other_members.sort_unstable();
18636
18637        let cell_refs: Vec<CellRef> = cell_members.iter().map(|(_, c)| *c).collect();
18638        let name_keys: Vec<String> = name_members.iter().map(|(_, k)| k.clone()).collect();
18639        let mut members: Vec<SccMember> = Vec::with_capacity(cycle.len());
18640        for (v, c) in &cell_members {
18641            members.push(SccMember {
18642                vertex: *v,
18643                cell: Some(*c),
18644            });
18645        }
18646        for (v, _) in &name_members {
18647            members.push(SccMember {
18648                vertex: *v,
18649                cell: None,
18650            });
18651        }
18652        for v in &other_members {
18653            members.push(SccMember {
18654                vertex: *v,
18655                cell: None,
18656            });
18657        }
18658        let n = members.len();
18659        // Indices addressable by the collector (cells + names); `other`
18660        // members can be neither edge sources nor targets.
18661        let recordable = cell_refs.len() + name_keys.len();
18662
18663        let circ_error = LiteralValue::Error(
18664            ExcelError::new(ExcelErrorKind::Circ)
18665                .with_message("Circular dependency detected".to_string()),
18666        );
18667
18668        // ── 0b. Spec-§4 persistence repair: structural edits clear computed
18669        // overlays wholesale (`clear_computed_overlay_after_row/_col`), but
18670        // an iterating member's committed value is cycle STATE, not a
18671        // recomputable cache — and in canonical mode the overlay is its ONLY
18672        // home. If the overlay entry vanished since the last recalc, re-seed
18673        // it from the end-of-recalc snapshot (`iterative_state_values`) so
18674        // pass-1 reads (scalar AND range, via the overlay cascade) observe
18675        // the persisted value instead of silently restarting at Empty→0.
18676        // (Found by the iterate edge corpus: inserting/deleting an unrelated
18677        // row reset accumulators, violating spec §4/§7.15.)
18678        if !self.iterative_state_values.is_empty() {
18679            let restore: Vec<(VertexId, LiteralValue)> = members
18680                .iter()
18681                .filter_map(|m| {
18682                    let cell = m.cell?;
18683                    let persisted = self.iterative_state_values.get(&m.vertex)?;
18684                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
18685                    let overlay = self
18686                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18687                        .unwrap_or(LiteralValue::Empty);
18688                    if matches!(overlay, LiteralValue::Empty) {
18689                        Some((m.vertex, persisted.clone()))
18690                    } else {
18691                        None
18692                    }
18693                })
18694                .collect();
18695            for (vertex, value) in restore {
18696                self.mirror_vertex_value_to_overlay(vertex, &value);
18697            }
18698        }
18699
18700        // ── 1. Pre-task value snapshot (overlay-first for cells — G3; the
18701        // graph value map may be evicted in value-cache-disabled mode).
18702        let snapshot: Vec<LiteralValue> = members
18703            .iter()
18704            .map(|m| match m.cell {
18705                Some(cell) => {
18706                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
18707                    self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18708                        .unwrap_or(LiteralValue::Empty)
18709                }
18710                None => self
18711                    .graph
18712                    .get_value(m.vertex)
18713                    .unwrap_or(LiteralValue::Empty),
18714            })
18715            .collect();
18716
18717        // ── 2. Pre-scan: spill anchors (FormulaArray) are stamped `#CIRC!`
18718        // with full spill teardown (spec §7.9, #115) and excluded from
18719        // evaluation. They stay recordable edge TARGETS (readers see `#CIRC!`
18720        // and propagate). Non-evaluable defensive members are excluded too.
18721        let mut excluded = vec![false; n];
18722        let mut last_value = snapshot.clone();
18723        let mut stamped = 0usize;
18724        for (i, m) in members.iter().enumerate() {
18725            match self.graph.get_vertex_kind(m.vertex) {
18726                VertexKind::FormulaArray => {
18727                    // Deltas for the cleared spill-region cells (non-members)
18728                    // can only be recorded here; the anchor's own delta is
18729                    // covered by the end-of-task snapshot comparison (dedup).
18730                    self.stamp_cycle_error(m.vertex, &circ_error, delta.as_deref_mut());
18731                    excluded[i] = true;
18732                    last_value[i] = circ_error.clone();
18733                    stamped += 1;
18734                }
18735                VertexKind::FormulaScalar | VertexKind::NamedScalar | VertexKind::NamedArray => {}
18736                _ => excluded[i] = true,
18737            }
18738        }
18739
18740        let collector = LiveEdgeCollector::new_with_names(&cell_refs, &name_keys);
18741
18742        // Per-member live out-edges, refreshed whenever a member re-runs.
18743        let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
18744        // Position of each member in the most recent pass (-1 = did not run).
18745        let mut pos: Vec<i64> = vec![-1; n];
18746        // Whether each member's committed value changed in the most recent pass.
18747        let mut changed = vec![false; n];
18748
18749        // Evaluate-and-commit one member; returns Ok(true) when the member was
18750        // stamped `#CIRC!` (array result — would-be spill anchor, spec §7.9).
18751        macro_rules! run_member {
18752            ($i:expr) => {{
18753                let i: usize = $i;
18754                let m = &members[i];
18755                if i < recordable {
18756                    collector.set_current(i as u32);
18757                }
18758                let value = {
18759                    let ctx = RecordingContext::new(&*self, &collector);
18760                    match self.evaluate_vertex_recorded(m.vertex, &ctx, &collector) {
18761                        Ok(v) => v,
18762                        Err(e) => LiteralValue::Error(e),
18763                    }
18764                };
18765                let is_cell_formula = m.cell.is_some();
18766                if is_cell_formula && matches!(value, LiteralValue::Array(_)) {
18767                    // A member that *would* spill inside an SCC gets the
18768                    // conservative §7.9 verdict. It has never spilled before
18769                    // (a prior spill would make it FormulaArray, pre-stamped
18770                    // above), so there is no projection to tear down.
18771                    self.stamp_cycle_error(m.vertex, &circ_error, None);
18772                    excluded[i] = true;
18773                    stamped += 1;
18774                    changed[i] = last_value[i] != circ_error;
18775                    last_value[i] = circ_error.clone();
18776                } else {
18777                    self.graph.update_vertex_value_ref(m.vertex, &value);
18778                    self.mirror_vertex_value_to_overlay(m.vertex, &value);
18779                    // §7.14 invariant (G2): a formula member must never be
18780                    // shadowed by a user/delta overlay entry, or iteration
18781                    // reads would silently diverge from committed values.
18782                    #[cfg(debug_assertions)]
18783                    if let Some(cell) = m.cell {
18784                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
18785                        debug_assert!(
18786                            self.read_delta_overlay_cell(
18787                                &sheet_name,
18788                                cell.coord.row() + 1,
18789                                cell.coord.col() + 1
18790                            )
18791                            .is_none(),
18792                            "user overlay must never shadow a formula SCC member ({sheet_name}!r{}c{})",
18793                            cell.coord.row() + 1,
18794                            cell.coord.col() + 1
18795                        );
18796                    }
18797                    changed[i] = last_value[i] != value;
18798                    last_value[i] = value;
18799                }
18800            }};
18801        }
18802
18803        let check_cancel = |flag: Option<&AtomicBool>| -> Result<(), ExcelError> {
18804            if let Some(flag) = flag
18805                && flag.load(Ordering::Relaxed)
18806            {
18807                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18808                    .with_message("Evaluation cancelled during SCC evaluation".to_string()));
18809            }
18810            Ok(())
18811        };
18812
18813        // ── 3. Pass 1: all evaluable members in member order.
18814        check_cancel(cancel_flag)?;
18815        let mut passes = 1usize;
18816        {
18817            let mut p = 0i64;
18818            for i in 0..n {
18819                if excluded[i] {
18820                    continue;
18821                }
18822                run_member!(i);
18823                pos[i] = p;
18824                p += 1;
18825            }
18826        }
18827
18828        // ── 4. Settle loop (design doc §3 step 4; RFC #113 policy arm).
18829        //
18830        // Acyclic classifications settle stale readers exactly (identical
18831        // under both policies — phantom SCCs never iterate). A witnessed
18832        // live cycle dispatches on policy: `Error` stamps `#CIRC!` and
18833        // stops; `Iterate` keeps running full passes over all members in
18834        // member order until converged (spec §6) or capped at
18835        // `max_iterations` total passes. A live cycle that only appears
18836        // mid-settle takes the same arm, and a cycle that dissolves
18837        // mid-iteration falls back to exact acyclic settling.
18838        //
18839        // Defensive acyclic budget: the acyclic settle is monotone, so more
18840        // than |SCC| + 2 settle passes can only be a bug; cap hits stamp the
18841        // remainder and set telemetry. Tracked via `settle_passes` so
18842        // iteration passes (legitimately many) don't consume the budget.
18843        let policy = self.config.cycle.policy;
18844        let cap = n + 2;
18845        let mut witnessed_cycles = 0usize;
18846        let mut capped = false;
18847        // ── Iterate-policy state ──
18848        let mut iterating = false;
18849        let mut converged = false;
18850        let mut exact_fixed_point = false;
18851        // Values committed by the last *full* pass; `None` until the first
18852        // iteration pass runs (pass 1 has no predecessor to compare against)
18853        // and reset when a settle pass runs (no cross-kind comparisons).
18854        let mut prev_pass: Option<Vec<LiteralValue>> = None;
18855        // Final-round convergence stats (overwritten per round so the values
18856        // reported are the ones observed at stop).
18857        let mut iter_max_delta = 0f64;
18858        let mut iter_nan_converged = 0usize;
18859        // Acyclic stale-reader re-eval passes (defensive budget; under pure
18860        // Error flow `1 + settle_passes == passes`, preserving Stage-2
18861        // behavior exactly).
18862        let mut settle_passes = 0usize;
18863        loop {
18864            // Drain this pass's recordings; members that ran replace their
18865            // out-edge set, members that didn't keep last-known edges.
18866            let drained = collector.take_edges();
18867            for i in 0..n {
18868                if pos[i] >= 0 {
18869                    out_edges[i].clear();
18870                }
18871            }
18872            for (from, to) in drained {
18873                debug_assert!(
18874                    pos[from as usize] >= 0,
18875                    "edge from a member that did not run"
18876                );
18877                out_edges[from as usize].push(to);
18878            }
18879            let mut edges: Vec<(u32, u32)> = Vec::new();
18880            for (i, outs) in out_edges.iter().enumerate() {
18881                if excluded[i] {
18882                    continue;
18883                }
18884                for &t in outs {
18885                    edges.push((i as u32, t));
18886                }
18887            }
18888            edges.sort_unstable();
18889            edges.dedup();
18890
18891            let analysis = analyze_live_graph(n, &edges);
18892
18893            if analysis.cycle_count > 0 {
18894                // Classification repeats every iteration pass under
18895                // `Iterate`; record the widest single witness instead of
18896                // accumulating so the count stays "distinct live cycles".
18897                witnessed_cycles = witnessed_cycles.max(analysis.cycle_count);
18898                match policy {
18899                    CyclePolicy::Error => {
18900                        // POLICY (Error): stamp every member of a live cycle,
18901                        // then one settling pass over the remaining members in
18902                        // live-topological order so error propagation
18903                        // downstream is consistent (spec §3.4). Blast radius =
18904                        // live cycles only.
18905                        for i in 0..n {
18906                            if analysis.in_cycle[i] && !excluded[i] {
18907                                self.stamp_cycle_error(members[i].vertex, &circ_error, None);
18908                                excluded[i] = true;
18909                                last_value[i] = circ_error.clone();
18910                                stamped += 1;
18911                            }
18912                        }
18913                        check_cancel(cancel_flag)?;
18914                        let order: Vec<usize> = analysis
18915                            .topo
18916                            .iter()
18917                            .map(|&i| i as usize)
18918                            .filter(|&i| !excluded[i])
18919                            .collect();
18920                        if !order.is_empty() {
18921                            passes += 1;
18922                            for i in order {
18923                                run_member!(i);
18924                            }
18925                        }
18926                        break;
18927                    }
18928                    CyclePolicy::Iterate {
18929                        max_iterations,
18930                        max_change,
18931                    } => {
18932                        // POLICY (Iterate), spec §3.5/§6.
18933                        iterating = true;
18934
18935                        // Convergence test: the full pass that just completed
18936                        // vs the previous full pass, per the spec-§6 rules.
18937                        // `prev_pass` is `None` until an iteration pass has
18938                        // run — pass 1 has no predecessor, so no convergence
18939                        // test occurs before the second pass (spec §6).
18940                        if let Some(prev) = &prev_pass {
18941                            let mut round_max_delta = 0f64;
18942                            let mut round_nan = 0usize;
18943                            let mut all_converged = true;
18944                            let mut round_exact = true;
18945                            for i in 0..n {
18946                                if excluded[i] {
18947                                    // Stamped mid-iteration (array result,
18948                                    // §7.9): the value is pinned and cannot
18949                                    // change again — trivially settled.
18950                                    continue;
18951                                }
18952                                let out = crate::engine::convergence::values_converged(
18953                                    &prev[i],
18954                                    &last_value[i],
18955                                    max_change,
18956                                    self.config.date_system,
18957                                );
18958                                if out.nan_converged {
18959                                    round_nan += 1;
18960                                }
18961                                if let Some(d) = out.abs_delta {
18962                                    round_max_delta = round_max_delta.max(d);
18963                                    if d != 0.0 {
18964                                        round_exact = false;
18965                                    }
18966                                }
18967                                if !out.converged {
18968                                    all_converged = false;
18969                                }
18970                            }
18971                            // Overwrite (not max): telemetry reports the
18972                            // round observed at stop.
18973                            iter_max_delta = round_max_delta;
18974                            iter_nan_converged = round_nan;
18975                            if all_converged {
18976                                exact_fixed_point = round_exact;
18977                                converged = true;
18978                                break;
18979                            }
18980                        }
18981
18982                        // ── Pass-counting reconciliation (spec §6/§7.6):
18983                        // `max_iterations` counts TOTAL passes, pass 1
18984                        // included, and pass 1 has already run by the time a
18985                        // live cycle is first witnessed here. The budget is
18986                        // therefore checked BEFORE evaluating anything more:
18987                        // with `max_iterations: 1` we stop right here — each
18988                        // member was evaluated exactly once this recalc (the
18989                        // Excel accumulator contract) and no convergence test
18990                        // ran (`prev_pass` is still `None`). Capping keeps
18991                        // the last committed values and is NOT an error
18992                        // (Excel parity); telemetry records it.
18993                        if passes >= max_iterations as usize {
18994                            capped = true;
18995                            break;
18996                        }
18997
18998                        check_cancel(cancel_flag)?;
18999                        // One more full pass over every evaluable member in
19000                        // member order (Gauss–Seidel: each commit is visible
19001                        // to later members within the pass). Live edges
19002                        // re-record — guards can flip near convergence
19003                        // (§7.3) — so classification repeats next time
19004                        // around, and a cycle that dissolves drops back to
19005                        // the exact acyclic settle below.
19006                        prev_pass = Some(last_value.clone());
19007                        for x in pos.iter_mut() {
19008                            *x = -1;
19009                        }
19010                        changed.fill(false);
19011                        passes += 1;
19012                        let mut p = 0i64;
19013                        for i in 0..n {
19014                            if excluded[i] {
19015                                continue;
19016                            }
19017                            run_member!(i);
19018                            pos[i] = p;
19019                            p += 1;
19020                        }
19021                        continue;
19022                    }
19023                }
19024            }
19025
19026            // Acyclic: find stale readers — members whose live read of `to`
19027            // happened before `to`'s value changed in the pass that just ran.
19028            let mut stale: Vec<usize> = Vec::new();
19029            for i in 0..n {
19030                if excluded[i] {
19031                    continue;
19032                }
19033                let is_stale = out_edges[i].iter().any(|&t| {
19034                    let t = t as usize;
19035                    changed[t] && (pos[i] < 0 || (pos[t] >= 0 && pos[i] < pos[t]))
19036                });
19037                if is_stale {
19038                    stale.push(i);
19039                }
19040            }
19041            if stale.is_empty() {
19042                break; // values exact — phantom SCC (or dissolved live cycle)
19043            }
19044            if 1 + settle_passes >= cap {
19045                // Defensive only; hitting this is a bug (loud telemetry).
19046                capped = true;
19047                for (i, m) in members.iter().enumerate() {
19048                    if !excluded[i] {
19049                        self.stamp_cycle_error(m.vertex, &circ_error, None);
19050                        excluded[i] = true;
19051                        last_value[i] = circ_error.clone();
19052                        stamped += 1;
19053                    }
19054                }
19055                break;
19056            }
19057
19058            check_cancel(cancel_flag)?;
19059            // Re-evaluate stale readers in live-topo order, recording fresh
19060            // edges (branches may flip on re-eval — spec §7.3 — which is why
19061            // classification repeats).
19062            // A settle pass is a partial sweep: drop the full-pass baseline
19063            // so a live cycle (re)appearing afterwards never compares values
19064            // across mixed pass kinds.
19065            prev_pass = None;
19066            let topo_pos = analysis.topo_positions();
19067            stale.sort_unstable_by_key(|&i| topo_pos[i]);
19068            for x in pos.iter_mut() {
19069                *x = -1;
19070            }
19071            changed.fill(false);
19072            passes += 1;
19073            settle_passes += 1;
19074            for (p, i) in stale.into_iter().enumerate() {
19075                run_member!(i);
19076                pos[i] = p as i64;
19077            }
19078        }
19079
19080        // Post-work boundary: members are committed write-through, so a
19081        // member that ran across a live cancellation is already visible; the
19082        // task still must not complete (no delta, retention or iteration
19083        // state). The members stay dirty for the retry, as with the
19084        // mid-task checks above.
19085        self.live_cancellation_after_work(SCC_CANCELLED)?;
19086
19087        // Iteration that ended because the live cycle dissolved and the
19088        // acyclic settle reached exactness counts as converged (values are
19089        // exact, strictly better than threshold-converged). The defensive
19090        // settle cap (`capped` + stamping) is not.
19091        if iterating && !converged && !capped {
19092            converged = true;
19093        }
19094
19095        // ── 5. End of task: one delta per member whose final value differs
19096        // from the pre-task snapshot (spec §3 side-effect rule, G11).
19097        collector.clear_current();
19098        if let Some(d) = delta
19099            && d.mode != DeltaMode::Off
19100        {
19101            for (i, m) in members.iter().enumerate() {
19102                if let Some(cell) = m.cell
19103                    && last_value[i] != snapshot[i]
19104                {
19105                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19106                }
19107            }
19108        }
19109
19110        // Members of an SCC that iterated re-evaluate on EVERY recalc, like
19111        // Excel's circular cells: register them for the end-of-recalc
19112        // volatile-like redirty (see `pending_iterative_redirty`). Marking
19113        // any one member propagates around the (strongly connected) SCC and
19114        // to downstream dependents, but all members are registered so the
19115        // contract survives partial structural edits between recalcs.
19116        //
19117        // Exception (#368): an SCC that stopped on an
19118        // exact fixed point — every member reproduced its previous value
19119        // bit-for-bit before the pass cap, no NaN identity, no volatile or
19120        // dynamic-reference member — cannot change on a re-run with the same
19121        // inputs, so it is retained clean. The dirty graph decides when it
19122        // runs again. Tolerance-only convergence (|Δ| < max_change but ≠ 0),
19123        // capped SCCs (including the `max_iterations: 1` accumulator
19124        // contract) and volatile cycles keep the per-recalc redirty.
19125        // Whatever the outcome, this task supersedes any earlier retention
19126        // of its members.
19127        if !self.retained_scc_members.is_empty() {
19128            for m in members.iter() {
19129                self.retained_scc_members.remove(&m.vertex);
19130            }
19131        }
19132        if iterating {
19133            let retain = converged
19134                && !capped
19135                && exact_fixed_point
19136                && iter_nan_converged == 0
19137                && members
19138                    .iter()
19139                    .all(|m| !self.graph.is_volatile(m.vertex) && !self.graph.is_dynamic(m.vertex));
19140            if retain {
19141                if self.retained_scc_members.is_empty() {
19142                    self.retained_scc_config_fingerprint = self.retained_scc_config_fingerprint();
19143                    self.retained_scc_function_epoch_seen =
19144                        crate::function_registry::semantic_epoch();
19145                    self.retained_scc_provider_revision_seen =
19146                        self.resolver.planning_semantic_revision();
19147                }
19148                let scc_id = self.next_retained_scc_id;
19149                self.next_retained_scc_id = self.next_retained_scc_id.wrapping_add(1);
19150                for (i, m) in members.iter().enumerate() {
19151                    self.retained_scc_members.insert(m.vertex, scc_id);
19152                    // §4 persistence snapshot, written once: retained
19153                    // members do not pass through `redirty_for_next_recalc`.
19154                    if matches!(last_value[i], LiteralValue::Empty) {
19155                        self.iterative_state_values.remove(&m.vertex);
19156                    } else {
19157                        self.iterative_state_values
19158                            .insert(m.vertex, last_value[i].clone());
19159                    }
19160                }
19161            } else {
19162                self.pending_iterative_redirty
19163                    .extend(members.iter().map(|m| m.vertex));
19164            }
19165        } else if !self.iterative_state_values.is_empty() {
19166            // The cycle dissolved (phantom settle or `#CIRC!` stamping):
19167            // these members are ordinary formulas again and must not carry
19168            // stale iteration state into a future cycle.
19169            for m in members.iter() {
19170                self.iterative_state_values.remove(&m.vertex);
19171            }
19172        }
19173
19174        {
19175            let t = &mut self.last_cycle_telemetry;
19176            t.static_sccs += 1;
19177            if witnessed_cycles == 0 && stamped == 0 && !capped {
19178                t.phantom_sccs += 1;
19179            }
19180            t.live_cycles_witnessed += witnessed_cycles;
19181            t.circ_cells_stamped += stamped;
19182            t.settle_passes_total += passes;
19183            t.max_passes_single_scc = t.max_passes_single_scc.max(passes);
19184            if iterating {
19185                t.iterated_sccs += 1;
19186                if converged {
19187                    t.converged_sccs += 1;
19188                }
19189                t.max_abs_delta_at_stop = t.max_abs_delta_at_stop.max(iter_max_delta);
19190                t.nan_converged += iter_nan_converged;
19191            }
19192            if capped {
19193                t.capped_sccs += 1;
19194            }
19195            t.elapsed_ms += task_start.elapsed().as_millis();
19196        }
19197
19198        Ok(stamped)
19199    }
19200
19201    /// Recorded sibling of [`Self::evaluate_vertex_immutable`]: evaluates one
19202    /// SCC member's AST via an [`Interpreter`] over a [`RecordingContext`] so
19203    /// reads that actually occur are captured as live edges. Value semantics
19204    /// must match `evaluate_vertex_immutable` exactly (including the missing-
19205    /// AST `Number(0.0)` quirk, G14); named Cell/Range/Literal definitions
19206    /// delegate to it after recording the definition region by hand (those
19207    /// reads bypass the context).
19208    fn evaluate_vertex_recorded(
19209        &self,
19210        vertex_id: VertexId,
19211        ctx: &RecordingContext<'_, R>,
19212        collector: &LiveEdgeCollector,
19213    ) -> Result<LiteralValue, ExcelError> {
19214        if !self.graph.vertex_exists(vertex_id) {
19215            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
19216                .with_message(format!("Vertex not found: {vertex_id:?}")));
19217        }
19218
19219        let kind = self.graph.get_vertex_kind(vertex_id);
19220        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
19221
19222        match kind {
19223            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19224                let Some(view) = self.graph.formula_view(vertex_id) else {
19225                    return Ok(LiteralValue::Number(0.0)); // G14 quirk
19226                };
19227                let sheet_name = self.graph.sheet_name(sheet_id);
19228                let cell_ref = self
19229                    .graph
19230                    .get_cell_ref(vertex_id)
19231                    .expect("cell ref for vertex");
19232                let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19233                interpreter
19234                    .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
19235                    .map(|cv| {
19236                        let format = cv.format_id();
19237                        self.record_derived_format(vertex_id, format);
19238                        crate::engine::result_finalization::finalize_published_calc_result(
19239                            cv,
19240                            self.config.spill.max_spill_cells,
19241                        )
19242                    })
19243            }
19244            VertexKind::NamedScalar | VertexKind::NamedArray => {
19245                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
19246                    ExcelError::new(ExcelErrorKind::Name)
19247                        .with_message("Named range metadata missing".to_string())
19248                })?;
19249
19250                match &named_range.definition {
19251                    NamedDefinition::Formula { ast, .. } => {
19252                        let context_sheet = match named_range.scope {
19253                            NameScope::Sheet(id) => id,
19254                            NameScope::Workbook => sheet_id,
19255                        };
19256                        let sheet_name = self.graph.sheet_name(context_sheet);
19257                        let cell_ref = self
19258                            .graph
19259                            .get_cell_ref(vertex_id)
19260                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
19261                        let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19262                        if kind == VertexKind::NamedScalar {
19263                            interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
19264                        } else {
19265                            match interpreter.evaluate_ast(ast) {
19266                                Ok(cv) => match cv.into_literal() {
19267                                    v @ LiteralValue::Array(_) => Ok(v),
19268                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
19269                                },
19270                                Err(err) => Ok(LiteralValue::Error(err)),
19271                            }
19272                        }
19273                    }
19274                    NamedDefinition::Cell(cell_ref) => {
19275                        // The definition is read via direct grid access in
19276                        // `evaluate_vertex_immutable`; record the live edge
19277                        // by hand before delegating.
19278                        collector.record_scalar(
19279                            cell_ref.sheet_id,
19280                            cell_ref.coord.row(),
19281                            cell_ref.coord.col(),
19282                        );
19283                        self.evaluate_vertex_immutable(vertex_id)
19284                    }
19285                    NamedDefinition::Range(range_ref) => {
19286                        if range_ref.start.sheet_id == range_ref.end.sheet_id {
19287                            collector.record_rect(
19288                                range_ref.start.sheet_id,
19289                                range_ref.start.coord.row(),
19290                                range_ref.start.coord.col(),
19291                                range_ref.end.coord.row(),
19292                                range_ref.end.coord.col(),
19293                            );
19294                        }
19295                        self.evaluate_vertex_immutable(vertex_id)
19296                    }
19297                    NamedDefinition::Literal(_) => self.evaluate_vertex_immutable(vertex_id),
19298                }
19299            }
19300            _ => self.evaluate_vertex_immutable(vertex_id),
19301        }
19302    }
19303
19304    /// Pending source occupancy is independent of formula preparation and value caches.
19305    fn pending_spill_occupied(&self, anchor: CellRef, end_row: u32, end_col: u32) -> bool {
19306        let sheet = self.graph.sheet_name(anchor.sheet_id);
19307        let package = self
19308            .staged_formulas
19309            .get(sheet)
19310            .and_then(|staged| staged.deferred_package.as_ref());
19311        self.staged_formula_index.occupies_spill(
19312            sheet,
19313            (anchor.coord.row() + 1, anchor.coord.col() + 1),
19314            (end_row + 1, end_col + 1),
19315            |point| package.is_some_and(|package| package.suppressed.contains(&point)),
19316        )
19317    }
19318
19319    fn remember_pending_spill(
19320        &mut self,
19321        vertex: VertexId,
19322        anchor: CellRef,
19323        region: Region,
19324    ) -> Result<(), ExcelError> {
19325        self.cancellation_checkpoint("pending spill occupancy")?;
19326        self.resource_checkpoint(1)?;
19327        if let Some(entry) = self
19328            .blocked_pending_spills
19329            .iter_mut()
19330            .find(|entry| entry.0 == vertex)
19331        {
19332            *entry = (vertex, anchor, region);
19333            return Ok(());
19334        }
19335        if self.blocked_pending_spills.len() == self.blocked_pending_spills.capacity() {
19336            // Geometric growth avoids copying every existing retry entry for
19337            // every new blocked anchor. Admit the entire capacity increment.
19338            let additional = self.blocked_pending_spills.capacity().max(1);
19339            let bytes =
19340                (additional as u64)
19341                    .saturating_mul(std::mem::size_of::<(VertexId, CellRef, Region)>() as u64);
19342            if let Some(ledger) = self.active_resource_ledger.as_mut() {
19343                ledger
19344                    .reserve_retained(bytes)
19345                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19346                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
19347            }
19348            if self
19349                .blocked_pending_spills
19350                .try_reserve_exact(additional)
19351                .is_err()
19352            {
19353                if let Some(ledger) = self.active_resource_ledger.as_mut() {
19354                    ledger
19355                        .release_retained(bytes)
19356                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19357                    self.source_cache_accounted -= bytes;
19358                }
19359                return Err(crate::engine::ResourceLedgerError::Exhausted(
19360                    formualizer_common::ResourceExhaustionDetail {
19361                        reason: formualizer_common::ResourceExhaustionReason::RetainedMemory,
19362                        limit: u64::MAX,
19363                        observed: bytes,
19364                        request_id: None,
19365                    },
19366                )
19367                .into_excel_error());
19368            }
19369        }
19370        self.blocked_pending_spills.push((vertex, anchor, region));
19371        Ok(())
19372    }
19373
19374    // Successful edits wake only intersecting attempted regions. Keep the entry
19375    // until evaluation: logged edits may still roll back, and materializing a
19376    // pending formula must not destroy its anchor's occupancy retry information.
19377    fn invalidate_pending_spills(&mut self, scope: StructuralScope) {
19378        if let StructuralScope::RemovedSheet(sheet) = scope {
19379            self.blocked_pending_spills
19380                .retain(|entry| entry.1.sheet_id != sheet);
19381            return;
19382        }
19383        for &(vertex, anchor, region) in &self.blocked_pending_spills {
19384            let affected = match scope {
19385                StructuralScope::Cell { sheet, row, col } => {
19386                    region.intersects(&Region::point(sheet, row, col))
19387                }
19388                StructuralScope::Region(changed) => region.intersects(&changed),
19389                StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => {
19390                    region.sheet_id() == sheet
19391                }
19392                StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => true,
19393            };
19394            if affected
19395                && self.graph.vertex_exists(vertex)
19396                && self.graph.get_cell_ref(vertex) == Some(anchor)
19397                && matches!(
19398                    self.graph.get_vertex_kind(vertex),
19399                    VertexKind::FormulaScalar | VertexKind::FormulaArray
19400                )
19401            {
19402                self.graph.mark_vertex_dirty(vertex);
19403            }
19404        }
19405    }
19406
19407    fn guard_pending_spill_commit(
19408        &mut self,
19409        anchor_vertex: VertexId,
19410        targets: &[CellRef],
19411    ) -> Result<(), ExcelError> {
19412        let Some(anchor) = self.graph.get_cell_ref(anchor_vertex) else {
19413            return Ok(());
19414        };
19415        let Some(last) = targets.last() else {
19416            return Ok(());
19417        };
19418        let occupied = self.pending_spill_occupied(anchor, last.coord.row(), last.coord.col());
19419        if (occupied
19420            || self
19421                .blocked_pending_spills
19422                .iter()
19423                .any(|entry| entry.0 == anchor_vertex))
19424            && let Err(error) = self.remember_pending_spill(
19425                anchor_vertex,
19426                anchor,
19427                Region::rect(
19428                    anchor.sheet_id,
19429                    anchor.coord.row(),
19430                    last.coord.row(),
19431                    anchor.coord.col(),
19432                    last.coord.col(),
19433                ),
19434            )
19435        {
19436            self.spill_mgr.release_owner(anchor_vertex);
19437            return Err(error);
19438        }
19439        if occupied {
19440            self.spill_mgr.release_owner(anchor_vertex);
19441            return Err(ExcelError::new(ExcelErrorKind::Spill)
19442                .with_message("Spill blocked")
19443                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
19444                    expected_rows: last.coord.row() - anchor.coord.row() + 1,
19445                    expected_cols: last.coord.col() - anchor.coord.col() + 1,
19446                }));
19447        }
19448        Ok(())
19449    }
19450
19451    /// Commit spill via shim and mirror resulting cells into Arrow overlay.
19452    fn commit_spill_and_mirror(
19453        &mut self,
19454        anchor_vertex: VertexId,
19455        targets: &[CellRef],
19456        rows: Vec<Vec<LiteralValue>>,
19457        delta: Option<&mut DeltaCollector>,
19458        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19459    ) -> Result<(), ExcelError> {
19460        self.guard_pending_spill_commit(anchor_vertex, targets)?;
19461        let prev_spill_cells = self
19462            .graph
19463            .spill_cells_for_anchor(anchor_vertex)
19464            .map(|cells| cells.to_vec())
19465            .unwrap_or_default();
19466
19467        if let Some(delta) = delta
19468            && delta.mode != DeltaMode::Off
19469        {
19470            let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
19471                targets.iter().copied().collect();
19472            let empty = LiteralValue::Empty;
19473
19474            // Clears (prev - targets)
19475            for cell in prev_spill_cells.iter() {
19476                if target_set.contains(cell) {
19477                    continue;
19478                }
19479                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19480                let old = self
19481                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19482                    .unwrap_or(LiteralValue::Empty);
19483                if old != empty {
19484                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19485                }
19486            }
19487
19488            // Writes (targets)
19489            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
19490                let width = rows[0].len();
19491                for (idx, cell) in targets.iter().enumerate() {
19492                    let r_off = idx / width;
19493                    let c_off = idx % width;
19494                    let new = rows
19495                        .get(r_off)
19496                        .and_then(|r| r.get(c_off))
19497                        .cloned()
19498                        .unwrap_or(LiteralValue::Empty);
19499                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19500                    let old = self
19501                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19502                        .unwrap_or(LiteralValue::Empty);
19503                    if old != new {
19504                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19505                    }
19506                }
19507            } else {
19508                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
19509                for cell in targets.iter() {
19510                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19511                    let old = self
19512                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19513                        .unwrap_or(LiteralValue::Empty);
19514                    if !matches!(old, LiteralValue::Empty) {
19515                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19516                    }
19517                }
19518            }
19519        }
19520
19521        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
19522        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
19523        let arrow_sheets = &self.arrow_sheets;
19524        self.spill_mgr.commit_array_with_value_probe(
19525            &mut self.graph,
19526            anchor_vertex,
19527            targets,
19528            rows.clone(),
19529            overwritable_formulas,
19530            |g, cell| {
19531                let sheet_name = g.sheet_name(cell.sheet_id);
19532                let asheet = arrow_sheets.sheet(sheet_name)?;
19533                let r0 = cell.coord.row() as usize;
19534                let c0 = cell.coord.col() as usize;
19535                let v = asheet.get_cell_value(r0, c0);
19536                if matches!(v, LiteralValue::Empty) {
19537                    None
19538                } else {
19539                    Some(v)
19540                }
19541            },
19542        )?;
19543
19544        self.blocked_pending_spills
19545            .retain(|entry| entry.0 != anchor_vertex);
19546        if let Some(scope) = Self::structural_scope_from_cells(&prev_spill_cells) {
19547            self.record_structural_change(scope);
19548        }
19549        if let Some(scope) = Self::structural_scope_from_cells(targets) {
19550            self.record_structural_change(scope);
19551        }
19552
19553        if self.config.arrow_storage_enabled
19554            && self.config.delta_overlay_enabled
19555            && self.config.write_formula_overlay_enabled
19556        {
19557            if !prev_spill_cells.is_empty() {
19558                let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
19559                    targets.iter().copied().collect();
19560                let empty = LiteralValue::Empty;
19561                for cell in prev_spill_cells.iter() {
19562                    if !target_set.contains(cell) {
19563                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19564                        self.mirror_value_to_computed_overlay(
19565                            &sheet_name,
19566                            cell.coord.row() + 1,
19567                            cell.coord.col() + 1,
19568                            &empty,
19569                        );
19570                    }
19571                }
19572            }
19573
19574            for (idx, cell) in targets.iter().enumerate() {
19575                if rows.is_empty() || rows[0].is_empty() {
19576                    break;
19577                }
19578                let width = rows[0].len();
19579                let r_off = idx / width;
19580                let c_off = idx % width;
19581                let v = rows[r_off][c_off].clone();
19582                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19583                self.mirror_value_to_computed_overlay(
19584                    &sheet_name,
19585                    cell.coord.row() + 1,
19586                    cell.coord.col() + 1,
19587                    &v,
19588                );
19589            }
19590        }
19591        Ok(())
19592    }
19593}
19594
19595#[cfg(test)]
19596#[path = "tests/authority_schedule_execution.rs"]
19597mod authority_schedule_execution;
19598
19599#[cfg(test)]
19600#[path = "tests/pending_spill.rs"]
19601mod pending_spill_tests;
19602
19603#[cfg(test)]
19604#[path = "tests/spill_batch_abort_192.rs"]
19605mod spill_batch_abort_192;
19606
19607// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
19608//
19609// Compute → Plan → Apply separation for evaluation side-effects.
19610
19611use crate::engine::effects::Effect;
19612use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
19613
19614impl<R> Engine<R>
19615where
19616    R: EvaluationContext,
19617{
19618    /// Plan effects for a single vertex after its value has been computed.
19619    ///
19620    /// This reads graph state but only performs lightweight mutations
19621    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
19622    /// during the planning phase.  Value-changing mutations are deferred to
19623    /// `apply_effect`.
19624    pub(crate) fn plan_vertex_effects(
19625        &mut self,
19626        vertex_id: VertexId,
19627        computed_value: LiteralValue,
19628        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19629    ) -> Result<Vec<Effect>, ExcelError> {
19630        // FR3: a stale dynamic reader publishes nothing; FR2: anything else
19631        // leaves the dirty set at its commit (design §8.2).
19632        if self.freshness_armed() {
19633            if self.freshness_drop_stale(vertex_id) {
19634                return Ok(Vec::new());
19635            }
19636            let effects = self.plan_vertex_effects_unrecorded(
19637                vertex_id,
19638                computed_value,
19639                overwritable_formulas,
19640            )?;
19641            self.freshness_mark_committed(vertex_id);
19642            return Ok(effects);
19643        }
19644        self.plan_vertex_effects_unrecorded(vertex_id, computed_value, overwritable_formulas)
19645    }
19646
19647    fn plan_vertex_effects_unrecorded(
19648        &mut self,
19649        vertex_id: VertexId,
19650        computed_value: LiteralValue,
19651        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19652    ) -> Result<Vec<Effect>, ExcelError> {
19653        let kind = self.graph.get_vertex_kind(vertex_id);
19654        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
19655
19656        // If this vertex's cell is currently covered by a spill from a different
19657        // anchor, ignore the computed result.  Formula vertices are exempt:
19658        // they must still evaluate so that overlapping spills produce #SPILL!.
19659        if !is_formula {
19660            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
19661                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
19662                && owner != vertex_id
19663            {
19664                return Ok(Vec::new());
19665            }
19666            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
19667            return Ok(vec![Effect::WriteCell {
19668                vertex_id,
19669                value: computed_value,
19670            }]);
19671        }
19672
19673        match computed_value {
19674            LiteralValue::Array(rows) => {
19675                self.plan_array_effects(vertex_id, rows, overwritable_formulas)
19676            }
19677            other => self.plan_scalar_effects(vertex_id, other),
19678        }
19679    }
19680
19681    /// Plan effects for a formula vertex that produced a scalar/error result.
19682    fn plan_scalar_effects(
19683        &mut self,
19684        vertex_id: VertexId,
19685        value: LiteralValue,
19686    ) -> Result<Vec<Effect>, ExcelError> {
19687        // Range admission substitutes the same cap error before allocating rows.
19688        // Preserve the owned-array rejection's kind, release and clear effects.
19689        if let LiteralValue::Error(error) = &value
19690            && error.kind == ExcelErrorKind::Spill
19691            && error.message.as_deref() == Some("SpillTooLarge")
19692            && let formualizer_common::ExcelErrorExtra::Spill {
19693                expected_rows,
19694                expected_cols,
19695            } = error.extra
19696            && u64::from(expected_rows).saturating_mul(u64::from(expected_cols))
19697                > u64::from(self.config.spill.max_spill_cells)
19698        {
19699            self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
19700            return self.plan_spill_error_effects(
19701                vertex_id,
19702                "SpillTooLarge",
19703                expected_rows,
19704                expected_cols,
19705            );
19706        }
19707        if !matches!(&value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Spill) {
19708            self.blocked_pending_spills
19709                .retain(|entry| entry.0 != vertex_id);
19710        }
19711        let has_spill = self
19712            .graph
19713            .spill_cells_for_anchor(vertex_id)
19714            .is_some_and(|c| !c.is_empty());
19715
19716        let mut effects = Vec::new();
19717        if has_spill {
19718            effects.push(Effect::SpillClear {
19719                anchor_vertex: vertex_id,
19720            });
19721        }
19722        effects.push(Effect::WriteCell { vertex_id, value });
19723        Ok(effects)
19724    }
19725
19726    /// Plan effects for a formula vertex that produced an array result.
19727    fn plan_array_effects(
19728        &mut self,
19729        vertex_id: VertexId,
19730        rows: Vec<Vec<LiteralValue>>,
19731        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
19732    ) -> Result<Vec<Effect>, ExcelError> {
19733        // Lightweight mutation needed for correct spill-blocking checks.
19734        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
19735
19736        let anchor = self
19737            .graph
19738            .get_cell_ref(vertex_id)
19739            .expect("cell ref for vertex");
19740        let sheet_id = anchor.sheet_id;
19741        let h = rows.len() as u32;
19742        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
19743
19744        // Hard cap to avoid vertex explosion from huge dynamic arrays.
19745        let spill_cells = (h as u64).saturating_mul(w as u64);
19746        if spill_cells > self.config.spill.max_spill_cells as u64 {
19747            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
19748        }
19749
19750        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
19751        const PACKED_MAX_ROW: u32 = 1_048_575;
19752        const PACKED_MAX_COL: u32 = 16_383;
19753        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
19754        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
19755        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
19756            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
19757        }
19758
19759        let mut targets = Vec::new();
19760        for r in 0..h {
19761            for c in 0..w {
19762                targets.push(self.graph.make_cell_ref_internal(
19763                    sheet_id,
19764                    anchor.coord.row() + r,
19765                    anchor.coord.col() + c,
19766                ));
19767            }
19768        }
19769
19770        if h != 0 && w != 0 {
19771            let occupied = self.pending_spill_occupied(anchor, end_row, end_col);
19772            if occupied
19773                || self
19774                    .blocked_pending_spills
19775                    .iter()
19776                    .any(|entry| entry.0 == vertex_id)
19777            {
19778                self.spill_mgr.release_owner(vertex_id);
19779                self.remember_pending_spill(
19780                    vertex_id,
19781                    anchor,
19782                    Region::rect(
19783                        sheet_id,
19784                        anchor.coord.row(),
19785                        end_row,
19786                        anchor.coord.col(),
19787                        end_col,
19788                    ),
19789                )?;
19790            }
19791            if occupied {
19792                return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
19793            }
19794        }
19795
19796        // Region lock via spill manager.
19797        match self.spill_mgr.reserve(
19798            vertex_id,
19799            anchor,
19800            SpillShape { rows: h, cols: w },
19801            SpillMeta {
19802                epoch: self.recalc_epoch,
19803                config: self.config.spill,
19804            },
19805        ) {
19806            Ok(()) => {
19807                // Validate spill region is available.
19808                if let Err(_e) = self.graph.plan_spill_region_allowing_formula_overwrite(
19809                    vertex_id,
19810                    &targets,
19811                    overwritable_formulas,
19812                ) {
19813                    return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
19814                }
19815
19816                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
19817                // cell values are not cached in the dependency graph. Consult Arrow storage to
19818                // detect occupied cells in the target region.
19819                if !self.graph.value_cache_enabled() {
19820                    let sheet_name = self.graph.sheet_name(sheet_id);
19821                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
19822                        for cell in targets.iter() {
19823                            // Allow overwriting the anchor itself.
19824                            if *cell == anchor {
19825                                continue;
19826                            }
19827                            // Allow cells already owned by a spill (plan() validated spill ownership).
19828                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
19829                                continue;
19830                            }
19831                            // Skip formula blockers; plan() handled them (or allowed).
19832                            if let Some(vid) = self.graph.get_vertex_id_for_address(cell)
19833                                && vid != vertex_id
19834                            {
19835                                match self.graph.get_vertex_kind(vid) {
19836                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19837                                        continue;
19838                                    }
19839                                    _ => {}
19840                                }
19841                            }
19842
19843                            let v = asheet.get_cell_value(
19844                                cell.coord.row() as usize,
19845                                cell.coord.col() as usize,
19846                            );
19847                            if !matches!(v, LiteralValue::Empty) {
19848                                return self.plan_spill_error_effects(
19849                                    vertex_id,
19850                                    "BlockedByValue",
19851                                    h,
19852                                    w,
19853                                );
19854                            }
19855                        }
19856                    }
19857                }
19858
19859                let top_left = rows
19860                    .first()
19861                    .and_then(|r| r.first())
19862                    .cloned()
19863                    .unwrap_or(LiteralValue::Empty);
19864
19865                let mut effects = Vec::new();
19866                // Clear previous spill if any.
19867                let has_prev = self
19868                    .graph
19869                    .spill_cells_for_anchor(vertex_id)
19870                    .is_some_and(|c| !c.is_empty());
19871                if has_prev {
19872                    effects.push(Effect::SpillClear {
19873                        anchor_vertex: vertex_id,
19874                    });
19875                }
19876                effects.push(Effect::SpillCommit {
19877                    anchor_vertex: vertex_id,
19878                    anchor_cell: anchor,
19879                    target_cells: targets,
19880                    values: rows,
19881                });
19882                effects.push(Effect::WriteCell {
19883                    vertex_id,
19884                    value: top_left,
19885                });
19886                Ok(effects)
19887            }
19888            Err(e) => {
19889                let msg = e.message.unwrap_or_else(|| "Spill blocked".to_string());
19890                self.plan_spill_error_effects(vertex_id, &msg, h, w)
19891            }
19892        }
19893    }
19894
19895    /// Build the effect list for a spill that failed validation.
19896    fn plan_spill_error_effects(
19897        &mut self,
19898        vertex_id: VertexId,
19899        message: &str,
19900        expected_rows: u32,
19901        expected_cols: u32,
19902    ) -> Result<Vec<Effect>, ExcelError> {
19903        self.spill_mgr.release_owner(vertex_id);
19904        let spill_err = ExcelError::new(ExcelErrorKind::Spill)
19905            .with_message(message)
19906            .with_extra(formualizer_common::ExcelErrorExtra::Spill {
19907                expected_rows,
19908                expected_cols,
19909            });
19910        let spill_val = LiteralValue::Error(spill_err);
19911
19912        let effects = vec![
19913            Effect::SpillClear {
19914                anchor_vertex: vertex_id,
19915            },
19916            Effect::WriteCell {
19917                vertex_id,
19918                value: spill_val,
19919            },
19920        ];
19921        Ok(effects)
19922    }
19923
19924    /// Apply a single effect, performing the actual graph mutations.
19925    pub(crate) fn apply_effect(
19926        &mut self,
19927        effect: &Effect,
19928        delta: Option<&mut DeltaCollector>,
19929        log: Option<&mut ChangeLog>,
19930    ) -> Result<(), ExcelError> {
19931        self.apply_effect_with_computed_writes(effect, delta, log, None)
19932    }
19933
19934    fn apply_effect_with_computed_writes(
19935        &mut self,
19936        effect: &Effect,
19937        delta: Option<&mut DeltaCollector>,
19938        log: Option<&mut ChangeLog>,
19939        computed_writes: Option<&mut ComputedWriteBuffer>,
19940    ) -> Result<(), ExcelError> {
19941        match effect {
19942            Effect::WriteCell { vertex_id, value } => {
19943                self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
19944            }
19945            Effect::SpillClear { anchor_vertex } => {
19946                self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
19947            }
19948            Effect::SpillCommit {
19949                anchor_vertex,
19950                anchor_cell: _,
19951                target_cells,
19952                values,
19953            } => {
19954                self.apply_spill_commit(
19955                    *anchor_vertex,
19956                    target_cells,
19957                    values.clone(),
19958                    delta,
19959                    log,
19960                    computed_writes,
19961                )?;
19962            }
19963        }
19964        Ok(())
19965    }
19966
19967    /// Apply a WriteCell effect.
19968    fn apply_write_cell(
19969        &mut self,
19970        vertex_id: VertexId,
19971        value: &LiteralValue,
19972        delta: Option<&mut DeltaCollector>,
19973        mut computed_writes: Option<&mut ComputedWriteBuffer>,
19974    ) -> Result<(), ExcelError> {
19975        if let Some(d) = delta
19976            && d.mode != DeltaMode::Off
19977        {
19978            if let Some(buffer) = computed_writes.as_deref_mut() {
19979                self.flush_computed_write_buffer(buffer)?;
19980            }
19981            if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
19982                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19983                let old = self
19984                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19985                    .unwrap_or(LiteralValue::Empty);
19986                if old != *value {
19987                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19988                }
19989            }
19990        }
19991        self.graph.update_vertex_value_ref(vertex_id, value);
19992        self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)?;
19993        Ok(())
19994    }
19995
19996    /// Apply a SpillClear effect.
19997    fn apply_spill_clear(
19998        &mut self,
19999        anchor_vertex: VertexId,
20000        delta: Option<&mut DeltaCollector>,
20001        log: Option<&mut ChangeLog>,
20002        computed_writes: Option<&mut ComputedWriteBuffer>,
20003    ) -> Result<(), ExcelError> {
20004        if let Some(buffer) = computed_writes {
20005            self.flush_computed_write_buffer(buffer)?;
20006        }
20007
20008        let spill_cells = self
20009            .graph
20010            .spill_cells_for_anchor(anchor_vertex)
20011            .map(|cells| cells.to_vec())
20012            .unwrap_or_default();
20013        if spill_cells.is_empty() {
20014            return Ok(());
20015        }
20016
20017        // Snapshot for ChangeLog before clearing.
20018        let snapshot = if log.is_some() {
20019            self.snapshot_spill_for_anchor(anchor_vertex)
20020        } else {
20021            None
20022        };
20023
20024        // Record delta for cleared cells.
20025        if let Some(d) = delta
20026            && d.mode != DeltaMode::Off
20027        {
20028            let empty = LiteralValue::Empty;
20029            for cell in spill_cells.iter() {
20030                let sheet_name = self.graph.sheet_name(cell.sheet_id);
20031                let old = self
20032                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20033                    .unwrap_or(LiteralValue::Empty);
20034                if old != empty {
20035                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20036                }
20037            }
20038        }
20039
20040        self.graph.clear_spill_region(anchor_vertex);
20041        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
20042            self.record_structural_change(scope);
20043        }
20044
20045        // Mirror Empty to Arrow overlay for cleared cells.
20046        if self.config.arrow_storage_enabled
20047            && self.config.delta_overlay_enabled
20048            && self.config.write_formula_overlay_enabled
20049        {
20050            let empty = LiteralValue::Empty;
20051            for cell in spill_cells.iter() {
20052                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20053                self.mirror_value_to_computed_overlay(
20054                    &sheet_name,
20055                    cell.coord.row() + 1,
20056                    cell.coord.col() + 1,
20057                    &empty,
20058                );
20059            }
20060        }
20061
20062        // ChangeLog.
20063        if let Some(log) = log
20064            && let Some(old) = snapshot
20065        {
20066            log.record(ChangeEvent::SpillCleared {
20067                anchor: anchor_vertex,
20068                old,
20069            });
20070        }
20071        Ok(())
20072    }
20073
20074    /// Apply a SpillCommit effect.
20075    fn apply_spill_commit(
20076        &mut self,
20077        anchor_vertex: VertexId,
20078        target_cells: &[CellRef],
20079        values: Vec<Vec<LiteralValue>>,
20080        delta: Option<&mut DeltaCollector>,
20081        log: Option<&mut ChangeLog>,
20082        computed_writes: Option<&mut ComputedWriteBuffer>,
20083    ) -> Result<(), ExcelError> {
20084        self.guard_pending_spill_commit(anchor_vertex, target_cells)?;
20085        if let Some(buffer) = computed_writes {
20086            self.flush_computed_write_buffer(buffer)?;
20087        }
20088
20089        // Snapshot for ChangeLog before commit.
20090        let old_snapshot = if log.is_some() {
20091            self.snapshot_spill_for_anchor(anchor_vertex)
20092        } else {
20093            None
20094        };
20095
20096        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
20097        self.commit_spill_and_mirror(
20098            anchor_vertex,
20099            target_cells,
20100            values.clone(),
20101            delta,
20102            None, // overwritable_formulas already validated in plan phase
20103        )?;
20104
20105        // ChangeLog.
20106        if let Some(log) = log {
20107            log.record(ChangeEvent::SpillCommitted {
20108                anchor: anchor_vertex,
20109                old: old_snapshot,
20110                new: SpillSnapshot {
20111                    target_cells: target_cells.to_vec(),
20112                    values,
20113                },
20114            });
20115        }
20116        Ok(())
20117    }
20118
20119    /// Snapshot a spill region for ChangeLog recording.
20120    ///
20121    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
20122    /// without creating a `VertexEditor`.
20123    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
20124        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
20125        if cells.is_empty() {
20126            return None;
20127        }
20128
20129        let max = self.config.spill.max_spill_cells as usize;
20130        let mut cells = cells;
20131        if cells.len() > max {
20132            cells.truncate(max);
20133        }
20134
20135        let first = *cells.first().expect("non-empty spill cells");
20136        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
20137        let row0 = first.coord.row();
20138        let col0 = first.coord.col();
20139
20140        let mut max_row = row0;
20141        let mut max_col = col0;
20142        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
20143        for cell in &cells {
20144            max_row = max_row.max(cell.coord.row());
20145            max_col = max_col.max(cell.coord.col());
20146            let v = self
20147                .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20148                .unwrap_or(LiteralValue::Empty);
20149            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
20150        }
20151
20152        let rows = (max_row - row0 + 1) as usize;
20153        let cols = (max_col - col0 + 1) as usize;
20154        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
20155        for r in 0..rows {
20156            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
20157            for c in 0..cols {
20158                row.push(
20159                    by_coord
20160                        .get(&(row0 + r as u32, col0 + c as u32))
20161                        .cloned()
20162                        .unwrap_or(LiteralValue::Empty),
20163                );
20164            }
20165            values.push(row);
20166        }
20167
20168        Some(SpillSnapshot {
20169            target_cells: cells,
20170            values,
20171        })
20172    }
20173
20174    fn flush_before_range_dependent_vertex(
20175        &mut self,
20176        vertex_id: VertexId,
20177        computed_writes: &mut ComputedWriteBuffer,
20178    ) -> Result<(), ExcelError> {
20179        if self.graph.reads_compressed_range(vertex_id) {
20180            self.flush_computed_write_buffer(computed_writes)?;
20181        }
20182        Ok(())
20183    }
20184
20185    fn plan_vertex_effects_with_computed_flush(
20186        &mut self,
20187        vertex_id: VertexId,
20188        computed_value: LiteralValue,
20189        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
20190        computed_writes: &mut ComputedWriteBuffer,
20191    ) -> Result<Vec<Effect>, ExcelError> {
20192        if matches!(&computed_value, LiteralValue::Array(_)) {
20193            self.flush_computed_write_buffer(computed_writes)?;
20194        }
20195        self.plan_vertex_effects(vertex_id, computed_value, overwritable_formulas)
20196    }
20197
20198    // ── Layer evaluation via effects pipeline ──────────────────────────────
20199
20200    fn evaluate_small_layer_direct_effects(
20201        &mut self,
20202        layer: &super::scheduler::Layer,
20203        delta: Option<&mut DeltaCollector>,
20204        log: Option<&mut ChangeLog>,
20205        cancel_flag: Option<&AtomicBool>,
20206        cancel_check_every: usize,
20207        cancel_message: &'static str,
20208    ) -> Result<usize, ExcelError> {
20209        let cancel = cancel_flag.map(|flag| (flag, cancel_check_every, cancel_message));
20210        self.evaluate_layer_units(layer, delta, log, cancel, false)
20211    }
20212
20213    /// Sequential layer walk over its units (single cells and family runs):
20214    /// each unit evaluates, then its vertices' effects apply in order. With
20215    /// `buffered`, computed writes coalesce in a layer buffer (flushed before
20216    /// a unit that reads a compressed range, before array results, and at
20217    /// the end); otherwise they apply directly. `cancel` = (flag, check every
20218    /// N vertices, message).
20219    fn evaluate_layer_units(
20220        &mut self,
20221        layer: &super::scheduler::Layer,
20222        delta: Option<&mut DeltaCollector>,
20223        log: Option<&mut ChangeLog>,
20224        cancel: Option<(&AtomicBool, usize, &'static str)>,
20225        buffered: bool,
20226    ) -> Result<usize, ExcelError> {
20227        self.evaluate_layer_units_until(layer, delta, log, cancel, buffered, None)
20228    }
20229
20230    /// [`Self::evaluate_layer_units`] that stops before the next unit once
20231    /// `stop_at` has passed; returns the vertices evaluated (a prefix of
20232    /// the layer, all committed).
20233    fn evaluate_layer_units_until(
20234        &mut self,
20235        layer: &super::scheduler::Layer,
20236        mut delta: Option<&mut DeltaCollector>,
20237        mut log: Option<&mut ChangeLog>,
20238        cancel: Option<(&AtomicBool, usize, &'static str)>,
20239        buffered: bool,
20240        stop_at: Option<crate::instant::FzInstant>,
20241    ) -> Result<usize, ExcelError> {
20242        // A chain unit: its run through the chain lift, or else cell by
20243        // cell in row order, each written before the next reads it.
20244        let mut chain_values = None;
20245        if layer.sequential && !layer.runs.is_empty() {
20246            chain_values = match layer.runs.as_slice() {
20247                [run] if run.start == 0 && run.len as usize == layer.vertices.len() => {
20248                    self.try_chain_lift(*run, &layer.vertices)
20249                }
20250                _ => None,
20251            };
20252            if chain_values.is_none() {
20253                let cells = super::scheduler::Layer {
20254                    vertices: layer.vertices.clone(),
20255                    runs: Vec::new(),
20256                    sequential: true,
20257                };
20258                return self.evaluate_layer_units_until(&cells, delta, log, cancel, false, stop_at);
20259            }
20260        }
20261        let chained = chain_values.is_some();
20262        // The chain lift computed every member: one block write, as a run.
20263        let buffered = buffered || chained;
20264        // A dynamic reader's targets are not always ordered before it (its
20265        // pre-probe or observed reads can miss them, e.g. after a structural
20266        // edit). In a buffered layer a member's dirty flag is cleared at its
20267        // commit but its value written at the flush: the members committed
20268        // since the last flush count as dirty for the reader's freshness
20269        // check, which then re-plans it after them.
20270        let track_unflushed = buffered
20271            && self.freshness_armed()
20272            && layer.vertices.iter().any(|&v| self.graph.is_dynamic(v));
20273        let mut committed_unit: &[VertexId] = &[];
20274        let mut computed_writes = ComputedWriteBuffer::default();
20275        let mut next_check = 0usize;
20276        let mut done = 0usize;
20277        for unit in layer_units(layer) {
20278            if done > 0
20279                && let Some(stop_at) = stop_at
20280                && crate::instant::FzInstant::now() >= stop_at
20281            {
20282                self.flush_computed_write_buffer(&mut computed_writes)?;
20283                if track_unflushed {
20284                    self.freshness_flushed();
20285                }
20286                return Ok(done);
20287            }
20288            if let Some((flag, every, message)) = cancel
20289                && every > 0
20290                && done >= next_check
20291            {
20292                next_check = (done / every + 1) * every;
20293                if flag.load(Ordering::Relaxed) {
20294                    if buffered {
20295                        self.flush_computed_write_buffer(&mut computed_writes)?;
20296                    }
20297                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
20298                        .with_message(message.to_string()));
20299                }
20300            }
20301            if buffered && self.unit_reads_compressed_range(layer, unit) {
20302                self.flush_computed_write_buffer(&mut computed_writes)?;
20303            }
20304            // The previous unit's members are committed (dirty flags
20305            // cleared); while their values wait in the buffer, a dynamic
20306            // reader's read of them is stale (`freshness_dirty_reads`).
20307            if track_unflushed {
20308                if computed_writes.is_empty() {
20309                    self.freshness_flushed();
20310                } else {
20311                    self.freshness_note_unflushed(committed_unit);
20312                }
20313                committed_unit = unit_members(layer, unit);
20314            }
20315            let values = match (chain_values.take(), unit) {
20316                (Some(chain), LayerUnit::Run(run)) => {
20317                    if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
20318                        self.flush_computed_write_buffer(&mut computed_writes)?;
20319                        return Err(e);
20320                    }
20321                    let members =
20322                        &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20323                    let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20324                    match self.commit_run_numbers(
20325                        run,
20326                        members,
20327                        &chain,
20328                        delta_active,
20329                        Some(&mut computed_writes),
20330                    ) {
20331                        Ok(true) => {
20332                            done += chain.len();
20333                            continue;
20334                        }
20335                        Ok(false) => {}
20336                        Err(e) => {
20337                            self.flush_computed_write_buffer(&mut computed_writes)?;
20338                            return Err(e);
20339                        }
20340                    }
20341                    members
20342                        .iter()
20343                        .copied()
20344                        .zip(chain.into_iter().map(LiteralValue::Number))
20345                        .collect()
20346                }
20347                (_, unit) => self.evaluate_unit_immutable(layer, unit),
20348            };
20349            // Post-work boundary: the unit ran while (or after) the request
20350            // was cancelled; it is not committed and stays dirty.
20351            if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
20352                self.flush_computed_write_buffer(&mut computed_writes)?;
20353                return Err(e);
20354            }
20355            done += values.len();
20356            if let LayerUnit::Run(run) = unit {
20357                let members = &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20358                let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20359                let committed = self.commit_run_scalars(
20360                    run,
20361                    members,
20362                    &values,
20363                    delta_active,
20364                    buffered.then_some(&mut computed_writes),
20365                );
20366                match committed {
20367                    Ok(true) => continue,
20368                    Ok(false) => {}
20369                    Err(e) => {
20370                        self.flush_computed_write_buffer(&mut computed_writes)?;
20371                        return Err(e);
20372                    }
20373                }
20374            }
20375            // A run unit's members were all evaluated before any commits
20376            // (FR5, FORM-192); a single cell is committed as it runs.
20377            let guarded = self.freshness_begin_batch_commit(&values);
20378            for (vertex_id, value) in values {
20379                let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20380                let effects = if buffered {
20381                    self.plan_vertex_effects_with_computed_flush(
20382                        vertex_id,
20383                        value,
20384                        None,
20385                        &mut computed_writes,
20386                    )
20387                } else {
20388                    self.plan_vertex_effects(vertex_id, value, None)
20389                };
20390                let effects = match effects {
20391                    Ok(effects) => effects,
20392                    Err(e) => {
20393                        self.flush_computed_write_buffer(&mut computed_writes)?;
20394                        return Err(e);
20395                    }
20396                };
20397                for effect in &effects {
20398                    if let Err(e) = self.apply_effect_with_computed_writes(
20399                        effect,
20400                        delta.as_deref_mut(),
20401                        log.as_deref_mut(),
20402                        buffered.then_some(&mut computed_writes),
20403                    ) {
20404                        self.flush_computed_write_buffer(&mut computed_writes)?;
20405                        return Err(e);
20406                    }
20407                }
20408                self.freshness_keep_redirtied(redirtied, vertex_id);
20409            }
20410        }
20411        self.flush_computed_write_buffer(&mut computed_writes)?;
20412        if track_unflushed {
20413            self.freshness_flushed();
20414        }
20415        // Debug builds: every chain member equals the per-cell path, now
20416        // that the members above it are written.
20417        #[cfg(debug_assertions)]
20418        if chained {
20419            for &v in &layer.vertices {
20420                let cell = self.graph.get_cell_ref(v);
20421                let (sheet, row, col) = cell
20422                    .map(|c| {
20423                        (
20424                            self.graph.sheet_name(c.sheet_id).to_string(),
20425                            c.coord.row() + 1,
20426                            c.coord.col() + 1,
20427                        )
20428                    })
20429                    .expect("chain member cell");
20430                let written = self.get_cell_value(&sheet, row, col);
20431                let oracle = self
20432                    .evaluate_vertex_immutable(v)
20433                    .unwrap_or_else(LiteralValue::Error);
20434                assert!(
20435                    written
20436                        .as_ref()
20437                        .is_some_and(|w| same_value_bits(w, &oracle)),
20438                    "chain member {sheet}!R{row}C{col}: {written:?} vs per-cell {oracle:?}"
20439                );
20440            }
20441        }
20442        #[cfg(not(debug_assertions))]
20443        let _ = chained;
20444        Ok(layer.vertices.len())
20445    }
20446
20447    /// Evaluate a layer sequentially using the effects pipeline.
20448    fn evaluate_layer_sequential_effects(
20449        &mut self,
20450        layer: &super::scheduler::Layer,
20451    ) -> Result<usize, ExcelError> {
20452        let buffered = buffer_layer_writes(layer);
20453        self.evaluate_layer_units(layer, None, None, None, buffered)
20454    }
20455
20456    /// Evaluate a layer sequentially with delta collection via effects pipeline.
20457    fn evaluate_layer_sequential_with_delta_effects(
20458        &mut self,
20459        layer: &super::scheduler::Layer,
20460        delta: &mut DeltaCollector,
20461    ) -> Result<usize, ExcelError> {
20462        let buffered = buffer_layer_writes(layer);
20463        self.evaluate_layer_units(layer, Some(delta), None, None, buffered)
20464    }
20465
20466    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
20467    fn evaluate_layer_sequential_cancellable_effects(
20468        &mut self,
20469        layer: &super::scheduler::Layer,
20470        cancel_flag: &AtomicBool,
20471    ) -> Result<usize, ExcelError> {
20472        let buffered = buffer_layer_writes(layer);
20473        let cancel = (cancel_flag, 256, "Evaluation cancelled within layer");
20474        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
20475    }
20476
20477    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
20478    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
20479        &mut self,
20480        layer: &super::scheduler::Layer,
20481        cancel_flag: &AtomicBool,
20482    ) -> Result<usize, ExcelError> {
20483        let buffered = buffer_layer_writes(layer);
20484        let cancel = (
20485            cancel_flag,
20486            128,
20487            "Demand-driven evaluation cancelled within layer",
20488        );
20489        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
20490    }
20491
20492    /// Evaluate a layer in parallel, applying via effects pipeline.
20493    fn evaluate_layer_parallel_effects(
20494        &mut self,
20495        layer: &super::scheduler::Layer,
20496        min_chunk: u32,
20497    ) -> Result<usize, ExcelError> {
20498        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20499
20500        let phases = self.parallel_phases(layer);
20501
20502        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20503        let mut applied = 0usize;
20504
20505        for (units, group) in &phases {
20506            let group = &group[..];
20507            if group.is_empty() {
20508                continue;
20509            }
20510            let mut computed_writes = ComputedWriteBuffer::default();
20511
20512            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
20513                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, min_chunk));
20514
20515            // Post-work boundary: a group evaluated across a live
20516            // cancellation is not committed (it stays dirty).
20517            let results = results.and_then(|results| {
20518                self.live_cancellation_after_work(GROUP_CANCELLED)
20519                    .map(|()| results)
20520            });
20521            // FR3: a parallel group is one commit unit; one stale reader
20522            // drops the whole group (it stays dirty and replans).
20523            self.freshness_gate_group(group);
20524            match results {
20525                Ok(vertex_results) => {
20526                    let (vertex_results, committed) = self.commit_parallel_runs(
20527                        layer,
20528                        units,
20529                        vertex_results,
20530                        false,
20531                        &mut computed_writes,
20532                    )?;
20533                    applied = applied.saturating_add(committed);
20534                    // Arrays first, then scalars — establishes spill regions before
20535                    // scalar results that might land inside a spilled region.
20536                    // FR5 (FORM-192): members evaluated together must not
20537                    // clear a re-dirty from a spill committed before them.
20538                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
20539                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20540                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20541                    for (vertex_id, result) in vertex_results {
20542                        if matches!(result, LiteralValue::Array(_)) {
20543                            arrays.push((vertex_id, result));
20544                        } else {
20545                            others.push((vertex_id, result));
20546                        }
20547                    }
20548                    for (vertex_id, result) in arrays {
20549                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20550                        let effects = match self.plan_vertex_effects_with_computed_flush(
20551                            vertex_id,
20552                            result,
20553                            Some(&inflight),
20554                            &mut computed_writes,
20555                        ) {
20556                            Ok(effects) => effects,
20557                            Err(e) => {
20558                                self.flush_computed_write_buffer(&mut computed_writes)?;
20559                                return Err(e);
20560                            }
20561                        };
20562                        for effect in &effects {
20563                            if let Err(e) = self.apply_effect_with_computed_writes(
20564                                effect,
20565                                None,
20566                                None,
20567                                Some(&mut computed_writes),
20568                            ) {
20569                                self.flush_computed_write_buffer(&mut computed_writes)?;
20570                                return Err(e);
20571                            }
20572                        }
20573                        self.freshness_keep_redirtied(redirtied, vertex_id);
20574                        applied = applied.saturating_add(1);
20575                    }
20576                    // Make all array spill/top-left writes visible before scalar effects in this group.
20577                    self.flush_computed_write_buffer(&mut computed_writes)?;
20578                    for (vertex_id, result) in others {
20579                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20580                        let effects = match self.plan_vertex_effects_with_computed_flush(
20581                            vertex_id,
20582                            result,
20583                            Some(&inflight),
20584                            &mut computed_writes,
20585                        ) {
20586                            Ok(effects) => effects,
20587                            Err(e) => {
20588                                self.flush_computed_write_buffer(&mut computed_writes)?;
20589                                return Err(e);
20590                            }
20591                        };
20592                        for effect in &effects {
20593                            if let Err(e) = self.apply_effect_with_computed_writes(
20594                                effect,
20595                                None,
20596                                None,
20597                                Some(&mut computed_writes),
20598                            ) {
20599                                self.flush_computed_write_buffer(&mut computed_writes)?;
20600                                return Err(e);
20601                            }
20602                        }
20603                        self.freshness_keep_redirtied(redirtied, vertex_id);
20604                        applied = applied.saturating_add(1);
20605                    }
20606                    // Flush at the group boundary; phase1 must be visible before phase2.
20607                    self.flush_computed_write_buffer(&mut computed_writes)?;
20608                }
20609                Err(e) => {
20610                    self.flush_computed_write_buffer(&mut computed_writes)?;
20611                    return Err(e);
20612                }
20613            }
20614        }
20615
20616        Ok(applied)
20617    }
20618
20619    /// Evaluate a layer in parallel with delta collection via effects pipeline.
20620    fn evaluate_layer_parallel_with_delta_effects(
20621        &mut self,
20622        layer: &super::scheduler::Layer,
20623        delta: &mut DeltaCollector,
20624    ) -> Result<usize, ExcelError> {
20625        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20626
20627        let phases = self.parallel_phases(layer);
20628
20629        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20630        let mut applied = 0usize;
20631
20632        for (units, group) in &phases {
20633            let group = &group[..];
20634            if group.is_empty() {
20635                continue;
20636            }
20637            let mut computed_writes = ComputedWriteBuffer::default();
20638            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
20639                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, 8));
20640
20641            // Post-work boundary: a group evaluated across a live
20642            // cancellation is not committed (it stays dirty).
20643            let results = results.and_then(|results| {
20644                self.live_cancellation_after_work(GROUP_CANCELLED)
20645                    .map(|()| results)
20646            });
20647            // FR3: a parallel group is one commit unit; one stale reader
20648            // drops the whole group (it stays dirty and replans).
20649            self.freshness_gate_group(group);
20650            match results {
20651                Ok(vertex_results) => {
20652                    let (vertex_results, committed) = self.commit_parallel_runs(
20653                        layer,
20654                        units,
20655                        vertex_results,
20656                        delta.mode != DeltaMode::Off,
20657                        &mut computed_writes,
20658                    )?;
20659                    applied = applied.saturating_add(committed);
20660                    // FR5 (FORM-192): members evaluated together must not
20661                    // clear a re-dirty from a spill committed before them.
20662                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
20663                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20664                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20665                    for (vertex_id, result) in vertex_results {
20666                        if matches!(result, LiteralValue::Array(_)) {
20667                            arrays.push((vertex_id, result));
20668                        } else {
20669                            others.push((vertex_id, result));
20670                        }
20671                    }
20672                    for (vertex_id, result) in arrays {
20673                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20674                        let effects = match self.plan_vertex_effects_with_computed_flush(
20675                            vertex_id,
20676                            result,
20677                            Some(&inflight),
20678                            &mut computed_writes,
20679                        ) {
20680                            Ok(effects) => effects,
20681                            Err(e) => {
20682                                self.flush_computed_write_buffer(&mut computed_writes)?;
20683                                return Err(e);
20684                            }
20685                        };
20686                        for effect in &effects {
20687                            if let Err(e) = self.apply_effect_with_computed_writes(
20688                                effect,
20689                                Some(delta),
20690                                None,
20691                                Some(&mut computed_writes),
20692                            ) {
20693                                self.flush_computed_write_buffer(&mut computed_writes)?;
20694                                return Err(e);
20695                            }
20696                        }
20697                        self.freshness_keep_redirtied(redirtied, vertex_id);
20698                        applied = applied.saturating_add(1);
20699                    }
20700                    self.flush_computed_write_buffer(&mut computed_writes)?;
20701                    for (vertex_id, result) in others {
20702                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20703                        let effects = match self.plan_vertex_effects_with_computed_flush(
20704                            vertex_id,
20705                            result,
20706                            Some(&inflight),
20707                            &mut computed_writes,
20708                        ) {
20709                            Ok(effects) => effects,
20710                            Err(e) => {
20711                                self.flush_computed_write_buffer(&mut computed_writes)?;
20712                                return Err(e);
20713                            }
20714                        };
20715                        for effect in &effects {
20716                            if let Err(e) = self.apply_effect_with_computed_writes(
20717                                effect,
20718                                Some(delta),
20719                                None,
20720                                Some(&mut computed_writes),
20721                            ) {
20722                                self.flush_computed_write_buffer(&mut computed_writes)?;
20723                                return Err(e);
20724                            }
20725                        }
20726                        self.freshness_keep_redirtied(redirtied, vertex_id);
20727                        applied = applied.saturating_add(1);
20728                    }
20729                    self.flush_computed_write_buffer(&mut computed_writes)?;
20730                }
20731                Err(e) => {
20732                    self.flush_computed_write_buffer(&mut computed_writes)?;
20733                    return Err(e);
20734                }
20735            }
20736        }
20737
20738        Ok(applied)
20739    }
20740
20741    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
20742    fn evaluate_layer_parallel_cancellable_effects(
20743        &mut self,
20744        layer: &super::scheduler::Layer,
20745        cancel_flag: &AtomicBool,
20746    ) -> Result<usize, ExcelError> {
20747        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
20748
20749        if cancel_flag.load(Ordering::Relaxed) {
20750            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
20751                .with_message("Parallel evaluation cancelled before starting".to_string()));
20752        }
20753
20754        let phases = self.parallel_phases(layer);
20755
20756        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
20757        let mut applied = 0usize;
20758
20759        for (units, group) in &phases {
20760            let group = &group[..];
20761            if group.is_empty() {
20762                continue;
20763            }
20764            let mut computed_writes = ComputedWriteBuffer::default();
20765
20766            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> = thread_pool
20767                .install(|| self.evaluate_units_parallel(layer, units, Some(cancel_flag), 8));
20768
20769            // Post-work boundary: a group evaluated across a live
20770            // cancellation is not committed (it stays dirty).
20771            let results = results.and_then(|results| {
20772                self.live_cancellation_after_work(GROUP_CANCELLED)
20773                    .map(|()| results)
20774            });
20775            // FR3: a parallel group is one commit unit; one stale reader
20776            // drops the whole group (it stays dirty and replans).
20777            self.freshness_gate_group(group);
20778            match results {
20779                Ok(vertex_results) => {
20780                    let (vertex_results, committed) = self.commit_parallel_runs(
20781                        layer,
20782                        units,
20783                        vertex_results,
20784                        false,
20785                        &mut computed_writes,
20786                    )?;
20787                    applied = applied.saturating_add(committed);
20788                    // FR5 (FORM-192): members evaluated together must not
20789                    // clear a re-dirty from a spill committed before them.
20790                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
20791                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
20792                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
20793                    for (vertex_id, result) in vertex_results {
20794                        if matches!(result, LiteralValue::Array(_)) {
20795                            arrays.push((vertex_id, result));
20796                        } else {
20797                            others.push((vertex_id, result));
20798                        }
20799                    }
20800                    for (vertex_id, result) in arrays {
20801                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20802                        let effects = match self.plan_vertex_effects_with_computed_flush(
20803                            vertex_id,
20804                            result,
20805                            Some(&inflight),
20806                            &mut computed_writes,
20807                        ) {
20808                            Ok(effects) => effects,
20809                            Err(e) => {
20810                                self.flush_computed_write_buffer(&mut computed_writes)?;
20811                                return Err(e);
20812                            }
20813                        };
20814                        for effect in &effects {
20815                            if let Err(e) = self.apply_effect_with_computed_writes(
20816                                effect,
20817                                None,
20818                                None,
20819                                Some(&mut computed_writes),
20820                            ) {
20821                                self.flush_computed_write_buffer(&mut computed_writes)?;
20822                                return Err(e);
20823                            }
20824                        }
20825                        self.freshness_keep_redirtied(redirtied, vertex_id);
20826                        applied = applied.saturating_add(1);
20827                    }
20828                    self.flush_computed_write_buffer(&mut computed_writes)?;
20829                    for (vertex_id, result) in others {
20830                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
20831                        let effects = match self.plan_vertex_effects_with_computed_flush(
20832                            vertex_id,
20833                            result,
20834                            Some(&inflight),
20835                            &mut computed_writes,
20836                        ) {
20837                            Ok(effects) => effects,
20838                            Err(e) => {
20839                                self.flush_computed_write_buffer(&mut computed_writes)?;
20840                                return Err(e);
20841                            }
20842                        };
20843                        for effect in &effects {
20844                            if let Err(e) = self.apply_effect_with_computed_writes(
20845                                effect,
20846                                None,
20847                                None,
20848                                Some(&mut computed_writes),
20849                            ) {
20850                                self.flush_computed_write_buffer(&mut computed_writes)?;
20851                                return Err(e);
20852                            }
20853                        }
20854                        self.freshness_keep_redirtied(redirtied, vertex_id);
20855                        applied = applied.saturating_add(1);
20856                    }
20857                    self.flush_computed_write_buffer(&mut computed_writes)?;
20858                }
20859                Err(e) => {
20860                    self.flush_computed_write_buffer(&mut computed_writes)?;
20861                    return Err(e);
20862                }
20863            }
20864        }
20865
20866        Ok(applied)
20867    }
20868
20869    // ── Top-level evaluate_all_logged ───────────────────────────────────────
20870
20871    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
20872    ///
20873    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
20874    /// every effect application so that spill commits/clears are captured.
20875    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
20876        self.observe_evaluation_resource_request(EvaluationRequestKind::FullLogged, |engine| {
20877            engine.evaluate_all_logged_unobserved(log)
20878        })
20879    }
20880
20881    fn evaluate_all_logged_unobserved(
20882        &mut self,
20883        log: &mut ChangeLog,
20884    ) -> Result<EvalResult, ExcelError> {
20885        self.observe_function_semantic_epoch()?;
20886        let _source_cache = self.source_cache_session();
20887        self.validate_deterministic_mode()?;
20888        if self.config.defer_graph_building {
20889            self.build_graph_all()?;
20890        }
20891        self.require_unified_authority()?;
20892        self.begin_evaluation_request();
20893        self.reset_virtual_dep_telemetry_if_disabled();
20894        let start = crate::instant::FzInstant::now();
20895        let mut computed_vertices = 0;
20896        let mut cycle_errors = 0;
20897
20898        let mut replan_iterations = 0;
20899        const MAX_REPLAN: usize = 5;
20900        let mut telemetry = self
20901            .config
20902            .enable_virtual_dep_telemetry
20903            .then(|| self.start_virtual_dep_telemetry());
20904
20905        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
20906
20907        let result = (|| -> Result<EvalResult, ExcelError> {
20908            loop {
20909                let to_evaluate = self.graph.get_evaluation_vertices();
20910                if to_evaluate.is_empty() {
20911                    if let Some(t) = telemetry.as_mut()
20912                        && t.bailout_reason.is_none()
20913                    {
20914                        t.bailout_reason = Some("no_work");
20915                    }
20916                    break;
20917                }
20918
20919                let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
20920                if let Some(t) = telemetry.as_mut() {
20921                    Self::accumulate_schedule_meta(t, &meta);
20922                }
20923
20924                // Walk units in condensation order: stamp cycles at their
20925                // position, evaluate layers with ChangeLog recording.
20926                self.begin_pass(&schedule);
20927                for (unit_index, &unit) in schedule.units.iter().enumerate() {
20928                    match unit {
20929                        ScheduleUnit::Cycle(i) => {
20930                            // Journal integration (design doc §4 last row): the
20931                            // ChangeLog in this path only records SpillClear /
20932                            // SpillCommit events; WriteCell effects are never
20933                            // logged (see `apply_write_cell`). Runtime SCC tasks
20934                            // write values directly and never spill (§7.9 stamps
20935                            // would-be anchors), and their spill *teardown* is the
20936                            // same unlogged `stamp_cycle_error` the Static path
20937                            // already uses here — so direct commits coexist with
20938                            // the journal cleanly, with identical semantics to
20939                            // Static. Pinned by `scc_runtime_cycles` tests.
20940                            if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0
20941                            {
20942                                cycle_errors += 1;
20943                            }
20944                        }
20945                        ScheduleUnit::Layer(i) => {
20946                            computed_vertices +=
20947                                self.evaluate_layer_logged(schedule.unit_layer(i), log)?;
20948                        }
20949                    }
20950                    if self.stop_after_unit(&schedule, unit_index) {
20951                        break;
20952                    }
20953                }
20954
20955                let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
20956                if let Some(t) = telemetry.as_mut() {
20957                    t.changed_vdeps_total += changed_vertices.len();
20958                }
20959                self.resource_checkpoint(0)?;
20960                if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
20961                    if let Some(t) = telemetry.as_mut() {
20962                        t.bailout_reason = Some("converged");
20963                    }
20964                    break;
20965                }
20966                if replan_iterations >= MAX_REPLAN {
20967                    if let Some(mut t) = telemetry.take() {
20968                        t.bailout_reason = Some("max_replan");
20969                        t.replan_iterations = replan_iterations;
20970                        self.last_virtual_dep_telemetry = t;
20971                    }
20972                    return Err(
20973                        self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
20974                    );
20975                }
20976                replan_iterations += 1;
20977            }
20978
20979            if let Some(mut t) = telemetry {
20980                t.replan_iterations = replan_iterations;
20981                self.last_virtual_dep_telemetry = t;
20982            }
20983
20984            self.redirty_for_next_recalc();
20985            self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
20986
20987            Ok(EvalResult {
20988                computed_vertices,
20989                cycle_errors,
20990                elapsed: start.elapsed(),
20991            })
20992        })();
20993        log.end_compound();
20994        result
20995    }
20996
20997    /// Evaluate a single layer with ChangeLog recording.
20998    fn evaluate_layer_logged(
20999        &mut self,
21000        layer: &super::scheduler::Layer,
21001        log: &mut ChangeLog,
21002    ) -> Result<usize, ExcelError> {
21003        self.resource_checkpoint(layer.vertices.len() as u64)?;
21004        self.evaluate_layer_units(layer, None, Some(log), None, true)
21005    }
21006}