1use crate::SheetId;
2
3mod criteria;
4mod criteria_mask_cache;
5#[cfg(test)]
6mod criteria_mask_cache_tests;
7mod exact;
8mod family;
9mod kernels;
10mod lift;
11mod memo;
12use crate::arrow_store::{OverlayFragment, OverlayValue, SheetStore};
13#[cfg(test)]
14use crate::engine::Scheduler;
15use crate::engine::arena::AstNodeId;
16use crate::engine::eval_delta::{
17 DeltaCollector, DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy,
18};
19use crate::engine::graph::editor::change_log::MutationCapture;
20use crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan;
21use crate::engine::ingest_pipeline::{DependencyPlanRow, FormulaAstInput};
22use crate::engine::live_edges::{LiveEdgeCollector, RecordingContext};
23use crate::engine::live_graph::analyze_live_graph;
24use crate::engine::lookup_index_cache::{
25 BuildOutcome, LookupAxis, LookupIndex, LookupIndexCache, LookupIndexCacheReport,
26 LookupIndexKey, estimate_bytes,
27};
28use crate::engine::named_range::{NameScope, NamedDefinition};
29use crate::engine::range_view::RangeView;
30use crate::engine::row_visibility::RowVisibilityState;
31use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
32use crate::engine::target_preparation::{
33 StagedFormulaIndex, StagedFormulaLease, StagedPackageLease,
34};
35use crate::engine::used_extent::{
36 ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
37};
38use crate::engine::virtual_deps::VirtualDepBuilder;
39use family::{LayerUnit, layer_units, unit_members};
40
41#[path = "freshness.rs"]
42mod freshness;
43use crate::engine::template::region::Region;
44use crate::engine::{
45 ChangeLogger, CycleDetection, CyclePolicy, DependencyGraph, EvalConfig, EvaluationRequestKind,
46 EvaluationRequestOutcome, EvaluationResourceBaselineStats, EvaluationResourceRequestStats,
47 FormulaDirtyLeaseOutcome, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
48 FormulaParseDiagnostic, FormulaParsePolicy, FormulaPlaneMode, ResourceLedger,
49 RowVisibilitySource, ScheduleUnit, VertexId, VertexKind, VisibilityMaskMode,
50};
51use crate::function::FnCaps;
52use crate::interpreter::Interpreter;
53
54#[cfg(test)]
55mod fn_snapshot_tests;
56use crate::reference::{CellRef, Coord, RangeRef};
57use crate::traits::FunctionProvider;
58use crate::traits::{EvaluationContext, ReferenceInfo, Resolver};
59use formualizer_common::{
60 CoordBuildHasher, LiteralValue, col_letters_from_1based, parse_a1_1based,
61};
62use formualizer_parse::parser::ReferenceType;
63use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind};
64use rayon::ThreadPoolBuilder;
65use rustc_hash::{FxHashMap, FxHashSet};
66use std::collections::{BTreeMap, BTreeSet, VecDeque};
67use std::sync::Arc;
68use std::sync::atomic::{AtomicBool, Ordering};
69use std::time::Instant;
70
71type StagedFormulaEntry = (u32, u32, String);
72type StagedSheetParts = (
73 Vec<StagedFormulaEntry>,
74 Option<crate::engine::DeferredFormulaPackage>,
75);
76
77#[derive(Default)]
87pub(crate) struct StagedSheet {
88 entries: Vec<StagedFormulaEntry>,
89 index: FxHashMap<(u32, u32), usize>,
90 deferred_package: Option<crate::engine::DeferredFormulaPackage>,
91}
92
93impl StagedSheet {
94 fn invalidate_all_deferred_families(package: &mut crate::engine::DeferredFormulaPackage) {
95 package
96 .invalidated
97 .extend(package.families.iter().map(|family| family.source_id));
98 package.invalidated.extend(
99 package
100 .partitioned_families
101 .iter()
102 .map(|family| family.source_id),
103 );
104 }
105
106 fn invalidate_deferred_at(&mut self, row: u32, col: u32) {
107 let Some(package) = self.deferred_package.as_mut() else {
108 return;
109 };
110 if package.suppressed.contains(&(row, col)) {
111 return;
114 }
115 if package.source_geometry_complete
116 && package.families.is_empty()
117 && package.partitioned_families.is_empty()
118 {
119 if let Some((row0, col0)) = row.checked_sub(1).zip(col.checked_sub(1))
120 && package
121 .source_coordinates
122 .binary_search(&crate::engine::SourceCoord {
123 row: row0,
124 col: col0,
125 })
126 .is_ok()
127 {
128 package.suppressed.insert((row, col));
129 }
130 return;
131 }
132 let family = package
133 .replay
134 .lock()
135 .ok()
136 .and_then(|mut replay| replay.formula_at(row, col).ok())
137 .flatten()
138 .and_then(|record| record.partition_owner.or(record.family));
139 if let Some(family) = family {
140 package.invalidated.insert(family);
141 } else {
142 Self::invalidate_all_deferred_families(package);
146 }
147 package.suppressed.insert((row, col));
148 }
149
150 fn reconcile_attached_deferred_package(&mut self) {
151 let Some(package) = self.deferred_package.as_mut() else {
152 return;
153 };
154 if self.entries.is_empty() {
155 return;
156 }
157
158 let replayed = (|| {
159 let mut disposition = crate::engine::FormulaReplayDisposition::default();
160 for partition in &package.partitioned_families {
161 disposition.register_partition(partition, false)?;
162 }
163 package
164 .replay
165 .lock()
166 .map_err(|_| "deferred formula spool lock poisoned".to_string())?
167 .replay_partitioned(&disposition, &package.partitioned_families)
168 })();
169
170 let Ok(records) = replayed else {
171 Self::invalidate_all_deferred_families(package);
172 package
173 .suppressed
174 .extend(self.entries.iter().map(|(row, col, _)| (*row, *col)));
175 return;
176 };
177 let mut owners = FxHashMap::default();
178 for record in &records {
179 owners
180 .entry((record.row, record.col))
181 .or_insert(record.partition_owner.or(record.family));
182 }
183 for (row, col, _) in &self.entries {
184 if let Some(family) = owners.get(&(*row, *col)).copied().flatten() {
185 package.invalidated.insert(family);
186 } else {
187 Self::invalidate_all_deferred_families(package);
188 }
189 package.suppressed.insert((*row, *col));
190 }
191 package.reconciliation_replay = Some(records);
192 }
193
194 fn stage(&mut self, row: u32, col: u32, text: String) {
195 self.invalidate_deferred_at(row, col);
196 match self.index.entry((row, col)) {
197 std::collections::hash_map::Entry::Occupied(slot) => {
198 self.entries[*slot.get()].2 = text;
199 }
200 std::collections::hash_map::Entry::Vacant(slot) => {
201 slot.insert(self.entries.len());
202 self.entries.push((row, col, text));
203 }
204 }
205 }
206
207 fn remove(&mut self, row: u32, col: u32) -> Option<String> {
208 let deferred_text = self.get(row, col);
209 self.invalidate_deferred_at(row, col);
210 let Some(idx) = self.index.remove(&(row, col)) else {
211 return deferred_text;
212 };
213 let (_, _, text) = self.entries.remove(idx);
214 for slot in self.index.values_mut() {
216 if *slot > idx {
217 *slot -= 1;
218 }
219 }
220 Some(text)
221 }
222
223 fn get_ordinary(&self, row: u32, col: u32) -> Option<&str> {
224 self.index
225 .get(&(row, col))
226 .map(|&i| self.entries[i].2.as_str())
227 }
228
229 fn remove_ordinary(&mut self, row: u32, col: u32) -> Option<String> {
230 let idx = self.index.remove(&(row, col))?;
231 let (_, _, text) = self.entries.remove(idx);
232 for slot in self.index.values_mut() {
233 if *slot > idx {
234 *slot -= 1;
235 }
236 }
237 Some(text)
238 }
239
240 fn get(&self, row: u32, col: u32) -> Option<String> {
241 if let Some(text) = self.get_ordinary(row, col) {
242 return Some(text.to_string());
243 }
244 let package = self.deferred_package.as_ref()?;
245 if package.suppressed.contains(&(row, col)) {
246 return None;
247 }
248 package
249 .replay
250 .lock()
251 .ok()?
252 .formula_at(row, col)
253 .ok()?
254 .map(|record| record.text)
255 }
256
257 fn len(&self) -> usize {
258 self.deferred_package
259 .as_ref()
260 .map_or(self.entries.len(), |package| {
261 (if package.source_geometry_complete
262 && (package.coordinates_cover_families
263 || (package.families.is_empty() && package.partitioned_families.is_empty()))
264 {
265 package.source_coordinates.len()
266 } else {
267 usize::try_from(package.report.source_formula_records_spooled)
268 .unwrap_or(usize::MAX)
269 })
270 .saturating_sub(package.suppressed.len())
271 .saturating_add(self.entries.len())
272 })
273 }
274
275 fn is_empty(&self) -> bool {
276 self.entries.is_empty() && self.deferred_package.is_none()
277 }
278
279 fn into_parts(self) -> StagedSheetParts {
280 (self.entries, self.deferred_package)
281 }
282}
283
284type StagedFormulaMap = std::collections::HashMap<String, StagedSheet>;
285
286#[derive(Clone, Debug, PartialEq, Eq, Hash)]
287struct PreparationRegion {
288 sheet: String,
289 sheet_id: SheetId,
290 start_row: u32,
291 start_col: u32,
292 end_row: u32,
293 end_col: u32,
294}
295
296struct OrderedTargetProducers {
298 ordered: Vec<crate::engine::target_preparation::TargetProducer>,
299 seen: FxHashSet<crate::engine::target_preparation::TargetProducer>,
300}
301
302impl OrderedTargetProducers {
303 fn with_capacity(capacity: usize) -> Result<Self, std::collections::TryReserveError> {
304 let mut ordered = Vec::new();
305 ordered.try_reserve(capacity)?;
306 let mut seen = FxHashSet::default();
307 seen.try_reserve(capacity)?;
308 Ok(Self { ordered, seen })
309 }
310
311 fn from_ordered(
312 ordered: Vec<crate::engine::target_preparation::TargetProducer>,
313 ) -> Result<Self, std::collections::TryReserveError> {
314 let mut seen = FxHashSet::default();
315 seen.try_reserve(ordered.len())?;
316 seen.extend(ordered.iter().copied());
317 Ok(Self { ordered, seen })
318 }
319
320 fn push(
321 &mut self,
322 producer: crate::engine::target_preparation::TargetProducer,
323 ) -> Result<bool, std::collections::TryReserveError> {
324 #[cfg(test)]
325 TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(probes.get().saturating_add(1)));
326 if self.seen.contains(&producer) {
327 return Ok(false);
328 }
329 self.seen.try_reserve(1)?;
330 self.ordered.try_reserve(1)?;
331 self.seen.insert(producer);
332 self.ordered.push(producer);
333 Ok(true)
334 }
335
336 fn len(&self) -> usize {
337 self.ordered.len()
338 }
339
340 fn into_vec(self) -> Vec<crate::engine::target_preparation::TargetProducer> {
341 self.ordered
342 }
343}
344
345#[cfg(test)]
346thread_local! {
347 static TARGET_ROOT_DEDUP_PROBES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
348}
349
350fn target_root_allocation_error(observed: usize, request_id: Option<u64>) -> ExcelError {
351 crate::engine::ResourceLedgerError::Exhausted(formualizer_common::ResourceExhaustionDetail {
352 reason: formualizer_common::ResourceExhaustionReason::ScratchMemory,
353 limit: u64::MAX,
354 observed: observed as u64,
355 request_id,
356 })
357 .into_excel_error()
358}
359
360#[derive(Debug)]
361struct PreparedOrdinaryStagedFormula {
362 sheet: String,
363 sheet_id: SheetId,
364 lease: StagedFormulaLease,
365 ast_id: Option<AstNodeId>,
366 plan: Option<DependencyPlanRow>,
367}
368
369struct PreparedTargetSourcePackage {
370 sheet: String,
371 sheet_id: SheetId,
372 lease: StagedPackageLease,
373 selected_points: Option<BTreeSet<(u32, u32)>>,
374 complete_selections: BTreeSet<crate::engine::SourceFamilyId>,
375 deferred_shared: bool,
376 direct_domains: Vec<(
377 crate::engine::SourceFamilyId,
378 crate::engine::PlacementDomainTransport,
379 )>,
380 source_report: crate::engine::FormulaCompressedSourceReport,
381 replay_records: Vec<crate::engine::DeferredReplayFormula>,
382 spool_replays: u64,
383 disposition: crate::engine::FormulaReplayDisposition,
384 legacy: Vec<(u32, u32, AstNodeId, DependencyPlanRow)>,
385 direct_families: usize,
386 direct_cells: u64,
387 direct_fragments: u64,
388 direct_complete_families: u64,
389 direct_complete_cells: u64,
390 direct_partition_families: u64,
391 direct_partition_cells: u64,
392 anchor_parses: u64,
393 anchor_asts: u64,
394 anchor_analyses: u64,
395}
396
397impl PreparedTargetSourcePackage {
398 fn empty_selection(sheet: &str, sheet_id: SheetId, lease: StagedPackageLease) -> Self {
399 Self {
400 sheet: sheet.to_owned(),
401 sheet_id,
402 lease,
403 selected_points: Some(BTreeSet::new()),
404 complete_selections: Default::default(),
405 deferred_shared: false,
406 direct_domains: Vec::new(),
407 source_report: Default::default(),
408 replay_records: Vec::new(),
409 spool_replays: 0,
410 disposition: Default::default(),
411 legacy: Vec::new(),
412 direct_families: 0,
413 direct_cells: 0,
414 direct_fragments: 0,
415 direct_complete_families: 0,
416 direct_complete_cells: 0,
417 direct_partition_families: 0,
418 direct_partition_cells: 0,
419 anchor_parses: 0,
420 anchor_asts: 0,
421 anchor_analyses: 0,
422 }
423 }
424
425 fn direct_contains(&self, row: u32, col: u32) -> bool {
426 self.direct_domains.iter().any(|(_, domain)| {
427 let rect = domain.rect();
428 row > rect.start.row
429 && row <= rect.end.row + 1
430 && col > rect.start.col
431 && col <= rect.end.col + 1
432 })
433 }
434
435 fn fallback_records(&self) -> impl Iterator<Item = &crate::engine::DeferredReplayFormula> {
436 self.replay_records.iter().filter(|record| {
437 let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1)) else {
438 return true;
439 };
440 let coord = crate::engine::SourceCoord { row, col };
441 let disposition = match record.family {
442 Some(family) => self.disposition.shared_disposition(family, coord),
443 None => self.disposition.ordinary_disposition(coord).0,
444 };
445 !matches!(
446 disposition,
447 crate::engine::FormulaReplayCoordinateDisposition::Direct
448 | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
449 )
450 })
451 }
452}
453
454type PreparedFormulaBatches = Vec<FormulaIngestBatch>;
455type StagedFormulaBatches = Vec<(String, StagedSheet)>;
456
457type CompressedReplayBatch = (
458 FormulaIngestBatch,
459 crate::engine::FormulaCompressedSourceBatch,
460);
461type PreparedSourceBatch = (
462 FormulaIngestBatch,
463 crate::engine::FormulaCompressedSourceReport,
464 crate::engine::FormulaCompressedPreparation,
465);
466type PreparedStagedFormulaBatches = (
467 PreparedFormulaBatches,
468 Vec<CompressedReplayBatch>,
469 Vec<PreparedSourceBatch>,
470 FxHashSet<crate::engine::arena::AstNodeId>,
472);
473
474#[doc(hidden)]
477pub struct SourceFormulaIngress<'a, R> {
478 engine: &'a mut Engine<R>,
479}
480
481impl<R> SourceFormulaIngress<'_, R>
482where
483 R: EvaluationContext,
484{
485 pub fn prepare_families(
486 &mut self,
487 sheet_name: &str,
488 families: &[crate::engine::SourceFormulaFamily],
489 ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
490 self.engine.ensure_not_poisoned()?;
491 self.engine.observe_function_semantic_epoch()?;
492 Ok(self
493 .engine
494 .prepare_source_formula_families(sheet_name, families))
495 }
496
497 pub fn prepare_eager_proposals(
498 &mut self,
499 sheet_name: &str,
500 families: &[crate::engine::SourceFormulaFamily],
501 partitions: &[crate::engine::PartitionedSourceFormulaFamily],
502 formula_record_count: u64,
503 replay: Box<dyn crate::engine::DeferredFormulaReplay>,
504 ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
505 self.engine.ensure_not_poisoned()?;
506 self.engine.observe_function_semantic_epoch()?;
507 let replay = Arc::new(std::sync::Mutex::new(replay));
508 self.engine.prepare_source_formula_proposals(
509 sheet_name,
510 families,
511 partitions,
512 partitions,
513 formula_record_count,
514 replay,
515 &BTreeSet::new(),
516 &Default::default(),
517 None,
518 )
519 }
520
521 pub fn stage_deferred(&mut self, package: crate::engine::DeferredFormulaPackage) {
525 self.engine.assert_not_poisoned();
526 self.engine.stage_deferred_formula_package(package);
527 }
528
529 pub fn ingest_replay_batches(
530 &mut self,
531 batches: Vec<(
532 FormulaIngestBatch,
533 crate::engine::FormulaCompressedSourceBatch,
534 )>,
535 ) -> Result<FormulaIngestReport, ExcelError> {
536 self.engine.ensure_not_poisoned()?;
537 self.engine
538 .ingest_compressed_formula_source_batches(batches)
539 }
540
541 pub fn finish_prepared(
542 &mut self,
543 batches: Vec<(
544 FormulaIngestBatch,
545 crate::engine::FormulaCompressedSourceReport,
546 crate::engine::FormulaCompressedPreparation,
547 )>,
548 ) -> Result<FormulaIngestReport, ExcelError> {
549 self.engine.ensure_not_poisoned()?;
550 self.engine.finish_compressed_formula_sources(batches)
551 }
552}
553
554const COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH: usize = 8;
558
559#[cfg(debug_assertions)]
561fn same_value_bits(a: &LiteralValue, b: &LiteralValue) -> bool {
562 match (a, b) {
563 (LiteralValue::Number(x), LiteralValue::Number(y)) => x.to_bits() == y.to_bits(),
564 _ => a == b,
565 }
566}
567
568fn buffer_layer_writes(layer: &crate::engine::scheduler::Layer) -> bool {
571 !layer.sequential && layer.vertices.len() >= COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH
572}
573const PARALLEL_SCHEDULE_MIN_CANDIDATES: usize = 16 * 1024;
581const BASE_SCHEDULE_MIN_VERTICES: usize = 64;
585const BASE_SCHEDULE_RATIO: usize = 256;
586const BASE_SCHEDULE_MIN_REQUEST: usize = 128;
591const PARALLEL_LAYER_PROBE: std::time::Duration = std::time::Duration::from_micros(300);
592const PARALLEL_LAYER_WORTH: std::time::Duration = std::time::Duration::from_micros(150);
593const EXPENSIVE_VERTEX_NS: u128 = 10_000;
595const UNIT_CANCELLED: &str = "Evaluation cancelled; the evaluated unit was not committed";
597const GROUP_CANCELLED: &str =
598 "Parallel evaluation cancelled; the evaluated group was not committed";
599const SCC_CANCELLED: &str = "Evaluation cancelled; the evaluated cycle was not completed";
600
601#[derive(Debug, Clone, PartialEq)]
602pub(crate) enum ComputedWrite {
603 Cell {
604 seq: u64,
605 sheet_id: SheetId,
606 row0: u32,
607 col0: u32,
608 value: OverlayValue,
609 format_id: Option<crate::format::FormatId>,
610 },
611 Rect {
612 seq: u64,
613 sheet_id: SheetId,
614 sr0: u32,
615 sc0: u32,
616 values: Vec<Vec<OverlayValue>>,
617 },
618 Run {
621 seq: u64,
622 sheet_id: SheetId,
623 row0: u32,
624 col0: u32,
625 entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
626 },
627}
628
629impl ComputedWrite {
630 #[inline]
631 pub(crate) fn seq(&self) -> u64 {
632 match self {
633 ComputedWrite::Cell { seq, .. }
634 | ComputedWrite::Rect { seq, .. }
635 | ComputedWrite::Run { seq, .. } => *seq,
636 }
637 }
638}
639
640#[derive(Debug)]
641struct BufferedReadClaim {
642 seq: u64,
643 result: Arc<crate::engine::effects::RecordedResult>,
644 winning: bool,
645}
646
647#[derive(Debug, Clone, Copy)]
648enum OverlayWriteDisposition {
649 Installed,
650 Buffered,
651 Skipped,
652}
653
654#[derive(Debug, Default)]
655pub(crate) struct ComputedWriteBuffer {
656 writes: Vec<ComputedWrite>,
657 next_seq: u64,
658 estimated_bytes: usize,
659 formats_present: bool,
660 read_claims: FxHashMap<(SheetId, u32, u32), BufferedReadClaim>,
663}
664
665impl ComputedWriteBuffer {
666 const ENTRY_BASE_BYTES: usize = 32;
667
668 #[inline]
669 pub(crate) fn is_empty(&self) -> bool {
670 self.writes.is_empty()
671 }
672
673 #[inline]
674 pub(crate) fn len(&self) -> usize {
675 self.writes.len()
676 }
677
678 #[inline]
679 pub(crate) fn writes(&self) -> &[ComputedWrite] {
680 &self.writes
681 }
682
683 #[inline]
684 pub(crate) fn estimated_bytes(&self) -> usize {
685 self.estimated_bytes
686 }
687
688 #[cfg(feature = "tracing")]
689 fn traced_cell_count(&self) -> usize {
690 self.writes
691 .iter()
692 .map(|write| match write {
693 ComputedWrite::Cell { .. } => 1,
694 ComputedWrite::Rect { values, .. } => values.iter().map(Vec::len).sum(),
695 ComputedWrite::Run { entries, .. } => entries.len(),
696 })
697 .sum()
698 }
699
700 pub(crate) fn push_cell(
701 &mut self,
702 sheet_id: SheetId,
703 row0: u32,
704 col0: u32,
705 value: OverlayValue,
706 ) {
707 self.push_cell_with_format(sheet_id, row0, col0, value, None);
708 }
709
710 pub(crate) fn push_cell_with_format(
711 &mut self,
712 sheet_id: SheetId,
713 row0: u32,
714 col0: u32,
715 value: OverlayValue,
716 format_id: Option<crate::format::FormatId>,
717 ) {
718 let format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
719 self.formats_present |= format_id.is_some();
720 let seq = self.next_sequence();
721 self.estimated_bytes = self
722 .estimated_bytes
723 .saturating_add(Self::estimate_value_bytes(&value));
724 self.writes.push(ComputedWrite::Cell {
725 seq,
726 sheet_id,
727 row0,
728 col0,
729 value,
730 format_id,
731 });
732 }
733
734 pub(crate) fn push_column_run(
735 &mut self,
736 sheet_id: SheetId,
737 row0: u32,
738 col0: u32,
739 mut entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
740 ) {
741 let seq = self.next_sequence();
742 let mut added = 0usize;
743 for (value, format_id) in entries.iter_mut() {
744 *format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
745 self.formats_present |= format_id.is_some();
746 added = added.saturating_add(Self::estimate_value_bytes(value));
747 }
748 self.estimated_bytes = self.estimated_bytes.saturating_add(added);
749 self.writes.push(ComputedWrite::Run {
750 seq,
751 sheet_id,
752 row0,
753 col0,
754 entries,
755 });
756 }
757
758 pub(crate) fn push_rect(
759 &mut self,
760 sheet_id: SheetId,
761 sr0: u32,
762 sc0: u32,
763 values: Vec<Vec<OverlayValue>>,
764 ) {
765 let seq = self.next_sequence();
766 let added = values
767 .iter()
768 .flat_map(|row| row.iter())
769 .map(Self::estimate_value_bytes)
770 .fold(0usize, usize::saturating_add);
771 self.estimated_bytes = self.estimated_bytes.saturating_add(added);
772 self.writes.push(ComputedWrite::Rect {
773 seq,
774 sheet_id,
775 sr0,
776 sc0,
777 values,
778 });
779 }
780
781 pub(crate) fn clear(&mut self) {
782 self.writes.clear();
783 self.read_claims.clear();
784 self.estimated_bytes = 0;
785 self.formats_present = false;
786 }
787
788 fn take_writes(&mut self) -> (Vec<ComputedWrite>, bool) {
789 self.estimated_bytes = 0;
790 let formats_present = std::mem::take(&mut self.formats_present);
791 (std::mem::take(&mut self.writes), formats_present)
792 }
793
794 fn next_sequence(&mut self) -> u64 {
795 let seq = self.next_seq;
796 self.next_seq = self.next_seq.wrapping_add(1);
797 seq
798 }
799
800 #[inline]
801 fn estimate_value_bytes(value: &OverlayValue) -> usize {
802 Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
803 }
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
807struct ComputedWriteChunkKey {
808 sheet_id: SheetId,
809 col0: u32,
810 chunk_idx: usize,
811 chunk_start_row0: u32,
812}
813
814#[derive(Debug, Clone, PartialEq)]
815pub(crate) struct ComputedWriteChunkEntryPlan {
816 pub(crate) row_in_chunk: usize,
817 pub(crate) seq: u64,
818 pub(crate) value: OverlayValue,
819 pub(crate) format_id: Option<crate::format::FormatId>,
820}
821
822#[derive(Debug, Clone, PartialEq, Eq)]
823pub(crate) enum ComputedWriteChunkPlanShape {
824 Point,
825 SparseOffsets {
826 entries: usize,
827 span_len: usize,
828 },
829 DenseRange {
830 start: usize,
831 len: usize,
832 },
833 RunRange {
834 start: usize,
835 len: usize,
836 runs: usize,
837 },
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub(crate) enum ComputedWriteFormatClear {
842 Range { start: usize, end: usize },
843 Offsets(Vec<usize>),
844}
845
846#[derive(Debug, Clone, PartialEq)]
847pub(crate) enum ComputedWriteChunkFormatEffect {
848 NoFormatWork,
849 ClearStale(ComputedWriteFormatClear),
850 SetExplicit(Vec<(usize, Option<crate::format::FormatId>)>),
851}
852
853#[derive(Debug, Clone, PartialEq)]
854pub(crate) struct ComputedWriteChunkPlan {
855 pub(crate) sheet_id: SheetId,
856 pub(crate) col0: u32,
857 pub(crate) chunk_idx: usize,
858 pub(crate) chunk_start_row0: u32,
859 pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
860 pub(crate) shape: ComputedWriteChunkPlanShape,
861 pub(crate) format_effect: ComputedWriteChunkFormatEffect,
862}
863
864#[derive(Debug, Clone, Default, PartialEq)]
865pub(crate) struct ComputedWriteCoalescingPlan {
866 pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
867 pub(crate) input_cells: usize,
868 pub(crate) coalesced_cells: usize,
869 pub(crate) overwritten_cells: usize,
870}
871
872impl ComputedWriteCoalescingPlan {
873 #[inline]
874 pub(crate) fn is_empty(&self) -> bool {
875 self.chunks.is_empty()
876 }
877}
878
879impl ComputedWriteChunkPlan {
880 fn from_group(
881 key: ComputedWriteChunkKey,
882 mut entries: Vec<ComputedWriteChunkEntryPlan>,
883 formats_present: bool,
884 computed_lane_has_formats: bool,
885 ) -> (Self, usize) {
886 entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
887 let input_len = entries.len();
888 let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
889 for entry in entries {
890 if let Some(prev) = coalesced.last_mut()
891 && prev.row_in_chunk == entry.row_in_chunk
892 {
893 *prev = entry;
894 continue;
895 }
896 coalesced.push(entry);
897 }
898 let overwritten = input_len.saturating_sub(coalesced.len());
899 let shape = Self::classify_shape(&coalesced);
900 let format_effect = Self::classify_format_effect(
901 &coalesced,
902 &shape,
903 formats_present,
904 computed_lane_has_formats,
905 );
906 (
907 Self {
908 sheet_id: key.sheet_id,
909 col0: key.col0,
910 chunk_idx: key.chunk_idx,
911 chunk_start_row0: key.chunk_start_row0,
912 entries: coalesced,
913 shape,
914 format_effect,
915 },
916 overwritten,
917 )
918 }
919
920 fn classify_format_effect(
921 entries: &[ComputedWriteChunkEntryPlan],
922 shape: &ComputedWriteChunkPlanShape,
923 formats_present: bool,
924 computed_lane_has_formats: bool,
925 ) -> ComputedWriteChunkFormatEffect {
926 if !formats_present {
927 return if computed_lane_has_formats {
928 ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
929 } else {
930 ComputedWriteChunkFormatEffect::NoFormatWork
931 };
932 }
933
934 if entries.iter().all(|entry| entry.format_id.is_none()) {
935 return if computed_lane_has_formats {
936 ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
937 } else {
938 ComputedWriteChunkFormatEffect::NoFormatWork
939 };
940 }
941
942 ComputedWriteChunkFormatEffect::SetExplicit(
943 entries
944 .iter()
945 .map(|entry| (entry.row_in_chunk, entry.format_id))
946 .collect(),
947 )
948 }
949
950 fn format_clear_spec(
951 entries: &[ComputedWriteChunkEntryPlan],
952 shape: &ComputedWriteChunkPlanShape,
953 ) -> ComputedWriteFormatClear {
954 match shape {
955 ComputedWriteChunkPlanShape::DenseRange { start, len }
956 | ComputedWriteChunkPlanShape::RunRange { start, len, .. } => {
957 ComputedWriteFormatClear::Range {
958 start: *start,
959 end: start.saturating_add(*len),
960 }
961 }
962 ComputedWriteChunkPlanShape::Point
963 | ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
964 ComputedWriteFormatClear::Offsets(
965 entries.iter().map(|entry| entry.row_in_chunk).collect(),
966 )
967 }
968 }
969 }
970
971 fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
972 debug_assert!(!entries.is_empty());
973 if entries.len() == 1 {
974 return ComputedWriteChunkPlanShape::Point;
975 }
976
977 let start = entries[0].row_in_chunk;
978 let end = entries[entries.len() - 1].row_in_chunk;
979 let span_len = end.saturating_sub(start).saturating_add(1);
980 if span_len != entries.len() {
981 return ComputedWriteChunkPlanShape::SparseOffsets {
982 entries: entries.len(),
983 span_len,
984 };
985 }
986
987 let runs = Self::run_count(entries);
988 if runs < entries.len() {
989 ComputedWriteChunkPlanShape::RunRange {
990 start,
991 len: entries.len(),
992 runs,
993 }
994 } else {
995 ComputedWriteChunkPlanShape::DenseRange {
996 start,
997 len: entries.len(),
998 }
999 }
1000 }
1001
1002 fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
1003 let mut runs = 0usize;
1004 let mut prev: Option<&OverlayValue> = None;
1005 for entry in entries {
1006 if prev != Some(&entry.value) {
1007 runs = runs.saturating_add(1);
1008 prev = Some(&entry.value);
1009 }
1010 }
1011 runs
1012 }
1013}
1014
1015#[cfg(feature = "tracing")]
1016#[derive(Default)]
1017struct TraceEvaluationCounters {
1018 computed_vertices: usize,
1019 cycles: usize,
1020}
1021
1022use crate::traits::{NamedRangeResolver as _, SourceResolver as _};
1023
1024pub struct Engine<R> {
1025 poisoned: Arc<std::sync::atomic::AtomicBool>,
1026 #[cfg(test)]
1027 pub(crate) panic_internal_once: bool,
1028 #[cfg(test)]
1029 pub(crate) panic_kernel_after_member: std::sync::atomic::AtomicUsize,
1030 criteria_mask_cache: criteria_mask_cache::CriteriaMaskCache,
1031 pub(crate) graph: DependencyGraph,
1032 resolver: crate::guarded_resolver::GuardedResolver<R>,
1033 pub(super) resolved_functions: crate::interpreter::ResolvedFunctions,
1034 pub(super) operation_diagnostic_collector:
1035 Option<Arc<crate::operation_diagnostics::OperationDiagnosticCollector>>,
1036 resolved_sheet_names:
1037 FxHashMap<crate::engine::arena::StringId, (crate::SheetId, Option<usize>)>,
1038 resolution_sheet_revision: Option<(u64, u64, usize)>,
1039 resolution_arrow_sheet_names: Vec<Arc<str>>,
1040 resolved_arrow_sheets: Vec<Option<usize>>,
1041 arrow_sheet_index: std::sync::OnceLock<Vec<Option<usize>>>,
1043 pub config: EvalConfig,
1044 workbook_load_limits: crate::engine::WorkbookLoadLimits,
1045 clock: crate::timezone::SnapshotClock,
1050 thread_pool: Option<Arc<rayon::ThreadPool>>,
1051 pub recalc_epoch: u64,
1052 resumable: Option<super::resumable::LogicalRecalculation>,
1053 resumable_call: bool,
1054 recalc_options: Option<super::RecalcJobOptions>,
1055 #[cfg(test)]
1056 pub(crate) resumable_stop_after_units_for_test: Option<usize>,
1057 snapshot_id: std::sync::atomic::AtomicU64,
1058 source_mutation_generation: u64,
1061 topology_epoch: u64,
1062 cached_static_schedule: Option<CachedScheduleEntry>,
1063 recent_schedules: Vec<CachedScheduleEntry>,
1068 base_schedule: Option<CachedScheduleEntry>,
1073 #[cfg(any(test, feature = "benchmark_internal"))]
1074 recalc_reuse_probe: std::sync::Mutex<RecalcReuseProbe>,
1075 spill_mgr: ShimSpillManager,
1076 arrow_sheets: SheetStore,
1078 format_registry: crate::format::FormatRegistry,
1080 derived_formats: crate::engine::derived_formats::DerivedFormats,
1082 #[cfg(test)]
1083 derived_format_operations_for_test: std::sync::atomic::AtomicU64,
1084 #[cfg(test)]
1085 family_members_for_test: std::sync::atomic::AtomicU64,
1086 #[cfg(test)]
1087 invariant_bound_members_for_test: std::sync::atomic::AtomicU64,
1088 #[cfg(test)]
1089 lifted_members_for_test: std::sync::atomic::AtomicU64,
1090 #[cfg(test)]
1091 chained_members_for_test: std::sync::atomic::AtomicU64,
1092 #[cfg(test)]
1093 lane_clean_reads_for_test: std::sync::atomic::AtomicU64,
1094 #[cfg(test)]
1095 criteria_kernel_members_for_test: std::sync::atomic::AtomicU64,
1096 #[cfg(test)]
1097 memo_hits_for_test: std::sync::atomic::AtomicU64,
1098 compressed_at_build: Option<u64>,
1100 #[cfg(test)]
1101 computed_overlay_set_explicit_entry_operations_for_test: u64,
1102 #[cfg(test)]
1103 computed_overlay_stale_clear_range_effects_for_test: u64,
1104 #[cfg(test)]
1105 computed_overlay_stale_clear_offset_attempts_for_test: u64,
1106 #[cfg(test)]
1107 computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64,
1108 has_edited: bool,
1110 overlay_compactions: u64,
1112
1113 computed_overlay_bytes_estimate: usize,
1115 computed_overlay_mirroring_disabled: bool,
1116 pub(crate) force_materialize_range_views: bool,
1119 row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
1121 used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
1123 lookup_index_cache: LookupIndexCache,
1124 source_cache: Arc<std::sync::RwLock<SourceCache>>,
1125 source_formula_token: Arc<()>,
1127 pub(crate) source_family_mode: crate::engine::SourceFamilyMode,
1129 pub(crate) source_family_counters: crate::engine::SourceFamilyCounters,
1130 recalc_plan_token: Arc<()>,
1132 staged_formulas: StagedFormulaMap,
1134 staged_formula_index: StagedFormulaIndex,
1136 blocked_pending_spills: Vec<(VertexId, CellRef, Region)>,
1138 spill_blocker_waiters: SpillBlockerWaiters,
1142 row_visibility: FxHashMap<SheetId, RowVisibilityState>,
1144 row_visibility_mask_cache: std::sync::RwLock<
1146 FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
1147 >,
1148 formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
1150 last_formula_ingest_report: Option<FormulaIngestReport>,
1152 formula_ingest_report_total: FormulaIngestReport,
1154 active_cancel_flag: Option<crate::engine::CancelToken>,
1156 active_evaluation_deadline: Option<Instant>,
1158
1159 action_depth: u32,
1164
1165 last_virtual_dep_telemetry: VirtualDepTelemetry,
1167 virtual_dep_fallback_activations: u64,
1168
1169 last_cycle_telemetry: CycleTelemetry,
1171
1172 next_evaluation_resource_request_id: u64,
1174 evaluation_resource_request_depth: usize,
1175 active_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1176 last_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1177 evaluation_resource_baseline: EvaluationResourceBaselineStats,
1178 evaluation_resource_request_started_at: Option<crate::instant::FzInstant>,
1179 evaluation_resource_budgets: crate::engine::EvaluationBudgets,
1180 evaluation_resource_config_diagnostic:
1181 Option<crate::engine::EvaluationResourceConfigDiagnostic>,
1182 active_resource_ledger: Option<ResourceLedger>,
1183 source_cache_footprints: Vec<std::sync::Weak<std::sync::atomic::AtomicU64>>,
1184 source_cache_accounted: u64,
1185
1186 pending_iterative_redirty: Vec<VertexId>,
1205 snapshot_evaluation_active: bool,
1207 snapshot_volatile_redirty_pending: bool,
1208 snapshot_iterative_redirty_pending: Vec<VertexId>,
1210 retained_scc_members: FxHashMap<VertexId, u64>,
1228 next_retained_scc_id: u64,
1229 retained_scc_config_fingerprint: u64,
1236 retained_scc_function_epoch_seen: u64,
1241 retained_scc_provider_revision_seen: Option<u64>,
1242 retained_scc_dirty_at_begin: Vec<(VertexId, u64)>,
1249
1250 iterative_state_values: FxHashMap<VertexId, LiteralValue>,
1263
1264 function_semantic_epoch_seen: u64,
1266 function_provider_revision_seen: Option<u64>,
1268
1269 #[cfg(feature = "tracing")]
1270 trace_evaluation_counters: TraceEvaluationCounters,
1271 #[cfg(test)]
1272 evaluation_request_begin_count_for_test: u64,
1273 #[cfg(any(test, feature = "test-support"))]
1274 before_prepared_span_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1275 #[cfg(test)]
1276 before_target_preparation_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1277 #[cfg(test)]
1278 before_target_planning_snapshot_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1279 #[cfg(test)]
1280 inject_target_semantic_stale_once_for_test: bool,
1281 #[cfg(test)]
1282 force_virtual_dep_changes_remaining_for_test: usize,
1283 freshness: freshness::Freshness,
1285 #[cfg(test)]
1286 fail_evaluation_commit_preflight_once_for_test: bool,
1287 #[cfg(test)]
1288 target_preparation_fault_for_test:
1289 Option<crate::engine::target_preparation::TargetPreparationFault>,
1290 #[cfg(test)]
1291 force_non_cycle_schedule_fallback_for_test: bool,
1292 #[cfg(test)]
1293 before_legacy_fallback_final_provider_sample_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1294 #[cfg(test)]
1295 after_eager_proposal_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1296}
1297
1298impl<R: EvaluationContext> Engine<R> {
1301 pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
1302 self.graph.ingest_pipeline(&self.resolver)
1303 }
1304}
1305
1306pub struct EngineAction<'a, R>
1307where
1308 R: EvaluationContext,
1309{
1310 engine: &'a mut Engine<R>,
1311 name: String,
1312 capture: Option<*mut MutationCapture>,
1315 arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
1318 atomic_policy: bool,
1320 spill_projection_history:
1325 std::collections::HashMap<CellRef, bool, formualizer_common::CoordBuildHasher>,
1326}
1327
1328impl<'a, R> EngineAction<'a, R>
1329where
1330 R: EvaluationContext,
1331{
1332 fn remember_spill_clears(&mut self, events: &[ChangeEvent]) {
1333 for event in events {
1334 if let ChangeEvent::SpillCleared { old, .. } = event {
1335 for &cell in &old.target_cells {
1336 self.spill_projection_history.entry(cell).or_insert(true);
1337 }
1338 }
1339 }
1340 }
1341
1342 fn spill_history_masks_before_insert(
1346 &self,
1347 sheet: SheetId,
1348 before: u32,
1349 rows: bool,
1350 ) -> Vec<CellRef> {
1351 let name = self.engine.graph.sheet_name(sheet);
1352 self.spill_projection_history
1353 .iter()
1354 .filter_map(|(&cell, &mask)| {
1355 let axis = if rows {
1356 cell.coord.row()
1357 } else {
1358 cell.coord.col()
1359 };
1360 (mask
1361 && cell.sheet_id == sheet
1362 && axis >= before
1363 && self
1364 .engine
1365 .read_cell_formula_ast(name, cell.coord.row() + 1, cell.coord.col() + 1)
1366 .is_none())
1367 .then_some(cell)
1368 })
1369 .collect()
1370 }
1371
1372 fn shift_spill_history(&mut self, sheet: SheetId, before: u32, count: u32, rows: bool) {
1373 self.spill_projection_history = self
1374 .spill_projection_history
1375 .drain()
1376 .map(|(cell, mask)| {
1377 if cell.sheet_id != sheet {
1378 return (cell, mask);
1379 }
1380 let (mut row, mut col) = (cell.coord.row(), cell.coord.col());
1381 if rows && row >= before {
1382 row = row.saturating_add(count);
1383 }
1384 if !rows && col >= before {
1385 col = col.saturating_add(count);
1386 }
1387 (CellRef::new(sheet, Coord::new(row, col, true, true)), mask)
1388 })
1389 .collect();
1390 }
1391
1392 #[inline]
1393 fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
1394 let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1395 let coord = crate::reference::Coord::from_excel(row, col, true, true);
1396 crate::reference::CellRef::new(sheet_id, coord)
1397 }
1398
1399 #[inline]
1400 pub fn name(&self) -> &str {
1401 &self.name
1402 }
1403
1404 #[inline]
1405 pub fn set_cell_value(
1406 &mut self,
1407 sheet: &str,
1408 row: u32,
1409 col: u32,
1410 value: LiteralValue,
1411 ) -> Result<(), crate::engine::EditorError> {
1412 self.engine.ensure_not_poisoned()?;
1413 if self.capture.is_some() {
1414 let old_value = self.engine.read_cell_value(sheet, row, col);
1415 let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1416 let addr = self.addr_for(sheet, row, col);
1417 let Some(capture_ptr) = self.capture else {
1418 return Err(crate::engine::EditorError::TransactionFailed {
1419 reason: "action_with_logger: missing mutation capture".to_string(),
1420 });
1421 };
1422
1423 let old_comp = if self.arrow_undo.is_some() {
1426 self.engine.read_computed_overlay_cell(sheet, row, col)
1427 } else {
1428 None
1429 };
1430
1431 if self.engine.graph_admission_enabled() {
1432 let admission =
1433 self.engine
1434 .graph
1435 .preview_value_mutation(addr.sheet_id, row, col)?;
1436 self.engine.preflight_graph_admission(admission)?;
1437 }
1438 if old_formula.is_none() {
1439 old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1440 }
1441
1442 let old_was_spill_child = self
1443 .engine
1444 .graph
1445 .spill_registry_anchor_for_cell(addr)
1446 .is_some_and(|owner| self.engine.graph.get_cell_ref(owner) != Some(addr));
1447 let delta_old_sem = if old_formula.is_some()
1448 || old_was_spill_child
1449 || self.spill_projection_history.get(&addr) == Some(&true)
1450 {
1451 None
1455 } else {
1456 Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1457 };
1458
1459 let start_len = unsafe { (&*capture_ptr).len() };
1460
1461 let capture = unsafe { &mut *capture_ptr };
1463 self.engine.edit_with_capture(capture, |editor| {
1464 editor.set_cell_value_with_old_state(
1465 addr,
1466 value.clone(),
1467 old_value.clone(),
1468 old_formula.clone(),
1469 );
1470 })?;
1471 self.engine.record_structural_change(StructuralScope::Cell {
1472 sheet: addr.sheet_id,
1473 row: addr.coord.row(),
1474 col: addr.coord.col(),
1475 });
1476
1477 self.remember_spill_clears(&unsafe { (&*capture_ptr).events() }[start_len..]);
1478 let clears_projection = matches!(value, LiteralValue::Empty)
1479 && self.spill_projection_history.contains_key(&addr);
1480 if clears_projection {
1481 self.engine.clear_delta_overlay_cell(sheet, row, col);
1485 self.engine.set_computed_overlay_cell_raw(
1486 sheet,
1487 row,
1488 col,
1489 Some(LiteralValue::Empty),
1490 );
1491 }
1492 if let Some(mask) = self.spill_projection_history.get_mut(&addr) {
1493 *mask = clears_projection;
1496 }
1497 if let Some(undo_ptr) = self.arrow_undo {
1498 let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1500 let undo = unsafe { &mut *undo_ptr };
1501 self.engine
1502 .record_spill_ops_into_arrow_undo(undo, new_events);
1503
1504 let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
1506 let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1507 let row0 = row.saturating_sub(1);
1508 let col0 = col.saturating_sub(1);
1509 let delta_new_sem = (!clears_projection).then_some(value.clone());
1510 undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
1511 undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
1512 }
1513 Ok(())
1514 } else {
1515 self.engine
1516 .set_cell_value(sheet, row, col, value)
1517 .map_err(crate::engine::EditorError::from)
1518 }
1519 }
1520
1521 #[inline]
1522 pub fn set_cell_formula(
1523 &mut self,
1524 sheet: &str,
1525 row: u32,
1526 col: u32,
1527 ast: ASTNode,
1528 ) -> Result<(), crate::engine::EditorError> {
1529 self.engine.ensure_not_poisoned()?;
1530 if self.capture.is_some() {
1531 let old_value = self.engine.read_cell_value(sheet, row, col);
1532 let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1533 let addr = self.addr_for(sheet, row, col);
1534 let Some(capture_ptr) = self.capture else {
1535 return Err(crate::engine::EditorError::TransactionFailed {
1536 reason: "action_with_logger: missing mutation capture".to_string(),
1537 });
1538 };
1539
1540 let admitted_formula = if self.engine.graph_admission_enabled() {
1541 let placement =
1542 CellRef::new(addr.sheet_id, Coord::from_excel(row, col, true, true));
1543 let ingested = self.engine.ingest_pipeline().ingest_formula(
1544 FormulaAstInput::Tree(ast.clone()),
1545 placement,
1546 None,
1547 )?;
1548 let admission = self.engine.graph.preview_formula_mutations(&[(
1549 addr.sheet_id,
1550 row,
1551 col,
1552 ingested.dep_plan.clone(),
1553 )])?;
1554 self.engine.preflight_graph_admission(admission)?;
1555 Some((ingested.ast_id, ingested.dep_plan))
1556 } else {
1557 None
1558 };
1559 if old_formula.is_none() {
1560 old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1561 }
1562 let delta_old = if self.arrow_undo.is_some() {
1567 if old_formula.is_some() || self.spill_projection_history.get(&addr) == Some(&true)
1568 {
1569 None
1570 } else {
1571 Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1572 }
1573 } else {
1574 None
1575 };
1576 let start_len = unsafe { (&*capture_ptr).len() };
1577
1578 let capture = unsafe { &mut *capture_ptr };
1580 self.engine.edit_with_capture(capture, |editor| {
1581 if let Some((ast_id, plan)) = admitted_formula {
1582 editor.set_cell_formula_with_prepared_plan(
1583 addr,
1584 ast.clone(),
1585 old_value,
1586 old_formula,
1587 ast_id,
1588 plan,
1589 );
1590 } else {
1591 editor.set_cell_formula_with_old_state(
1592 addr,
1593 ast.clone(),
1594 old_value,
1595 old_formula,
1596 );
1597 }
1598 })?;
1599 self.engine.record_structural_change(StructuralScope::Cell {
1600 sheet: addr.sheet_id,
1601 row: addr.coord.row(),
1602 col: addr.coord.col(),
1603 });
1604
1605 self.remember_spill_clears(&unsafe { (&*capture_ptr).events() }[start_len..]);
1606 if let Some(mask) = self.spill_projection_history.get_mut(&addr) {
1607 *mask = false;
1608 }
1609 if let Some(undo_ptr) = self.arrow_undo {
1610 let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1611 let undo = unsafe { &mut *undo_ptr };
1612 self.engine
1613 .record_spill_ops_into_arrow_undo(undo, new_events);
1614 let delta_new: Option<LiteralValue> = None;
1615 let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1616 let row0 = row.saturating_sub(1);
1617 let col0 = col.saturating_sub(1);
1618 undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
1619 }
1620 Ok(())
1621 } else {
1622 self.engine
1623 .set_cell_formula(sheet, row, col, ast)
1624 .map_err(crate::engine::EditorError::from)
1625 }
1626 }
1627
1628 pub fn resize_table(
1630 &mut self,
1631 name: &str,
1632 new_end_row: u32,
1633 ) -> Result<TableMetadata, crate::engine::EditorError> {
1634 self.resize_table_body(name, new_end_row, false)
1635 }
1636
1637 pub fn resize_table_body(
1639 &mut self,
1640 name: &str,
1641 new_end_row: u32,
1642 empty_body: bool,
1643 ) -> Result<TableMetadata, crate::engine::EditorError> {
1644 self.engine.ensure_not_poisoned()?;
1645 let range = self.engine.table_resize_range(name, new_end_row)?;
1646 if let Some(capture_ptr) = self.capture {
1647 self.engine
1648 .edit_with_capture(unsafe { &mut *capture_ptr }, |editor| {
1649 editor.resize_table_body(name, range, empty_body)
1650 })??;
1651 } else {
1652 return self
1653 .engine
1654 .resize_table_body(name, new_end_row, empty_body)
1655 .map_err(Into::into);
1656 }
1657 Ok(self
1658 .engine
1659 .table_metadata(name)
1660 .expect("resized table exists"))
1661 }
1662
1663 #[inline]
1664 pub fn set_row_hidden(
1665 &mut self,
1666 sheet: &str,
1667 row_1based: u32,
1668 hidden: bool,
1669 source: RowVisibilitySource,
1670 ) -> Result<(), crate::engine::EditorError> {
1671 self.engine.ensure_not_poisoned()?;
1672 if self.capture.is_some() {
1673 let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1674 let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
1675 let old_hidden = self
1676 .engine
1677 .row_visibility
1678 .get(&sheet_id)
1679 .map(|state| state.is_row_hidden(row0, Some(source)))
1680 .unwrap_or(false);
1681 if old_hidden == hidden {
1682 return Ok(());
1683 }
1684
1685 let _ = self
1686 .engine
1687 .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1688
1689 let Some(capture_ptr) = self.capture else {
1690 return Err(crate::engine::EditorError::TransactionFailed {
1691 reason: "action_with_logger: missing mutation capture".to_string(),
1692 });
1693 };
1694 unsafe { &mut *capture_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
1695 sheet_id,
1696 row0,
1697 source,
1698 old_hidden,
1699 new_hidden: hidden,
1700 });
1701
1702 Ok(())
1703 } else {
1704 self.engine
1705 .set_row_hidden(sheet, row_1based, hidden, source)
1706 }
1707 }
1708
1709 #[inline]
1710 pub fn set_rows_hidden(
1711 &mut self,
1712 sheet: &str,
1713 start_row_1based: u32,
1714 end_row_1based: u32,
1715 hidden: bool,
1716 source: RowVisibilitySource,
1717 ) -> Result<(), crate::engine::EditorError> {
1718 self.engine.ensure_not_poisoned()?;
1719 if self.capture.is_some() {
1720 let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1721 let (start_row0, end_row0) =
1722 Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
1723
1724 let Some(capture_ptr) = self.capture else {
1725 return Err(crate::engine::EditorError::TransactionFailed {
1726 reason: "action_with_logger: missing mutation capture".to_string(),
1727 });
1728 };
1729 let capture = unsafe { &mut *capture_ptr };
1730
1731 for row0 in start_row0..=end_row0 {
1732 let old_hidden = self
1733 .engine
1734 .row_visibility
1735 .get(&sheet_id)
1736 .map(|state| state.is_row_hidden(row0, Some(source)))
1737 .unwrap_or(false);
1738 if old_hidden == hidden {
1739 continue;
1740 }
1741
1742 let _ = self
1743 .engine
1744 .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1745
1746 capture.record(crate::engine::ChangeEvent::SetRowVisibility {
1747 sheet_id,
1748 row0,
1749 source,
1750 old_hidden,
1751 new_hidden: hidden,
1752 });
1753 }
1754
1755 Ok(())
1756 } else {
1757 self.engine
1758 .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
1759 }
1760 }
1761
1762 #[inline]
1763 pub fn insert_rows(
1764 &mut self,
1765 sheet: &str,
1766 before: u32,
1767 count: u32,
1768 ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1769 self.engine.ensure_not_poisoned()?;
1770 if count == 0 {
1771 return Ok(crate::engine::ShiftSummary::default());
1772 }
1773 if self.capture.is_some() {
1774 let Some(capture_ptr) = self.capture else {
1775 return Err(crate::engine::EditorError::TransactionFailed {
1776 reason: "action_atomic: missing mutation capture".to_string(),
1777 });
1778 };
1779
1780 let before0 = before.saturating_sub(1);
1781 self.engine.check_insert_room(sheet, before0, count, true)?;
1782 let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1783 let occupancy = self.engine.structural_row_occupancy(sheet, sheet_id);
1784 let affected_region = Engine::<R>::structural_row_region(sheet_id, before0);
1785 let mut spill_masks = self.spill_history_masks_before_insert(sheet_id, before0, true);
1786
1787 let cleared_spill_cells = self.engine.clear_spills_before_structural_edit(
1790 sheet_id,
1791 before0,
1792 true,
1793 Some(unsafe { &mut *capture_ptr }),
1794 self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1795 )?;
1796 spill_masks.extend(cleared_spill_cells.iter().copied());
1797
1798 let summary = {
1800 let capture = unsafe { &mut *capture_ptr };
1801 let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1802 Ok(crate::engine::ShiftSummary::default());
1803 self.engine.edit_with_capture(capture, |editor| {
1804 editor.set_structural_occupancy(occupancy);
1805 out = editor.insert_rows(sheet_id, before0, count);
1806 })?;
1807 out?
1808 };
1809
1810 self.engine.ensure_arrow_sheet(sheet);
1812 if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1813 asheet.insert_rows(before0 as usize, count as usize);
1814 }
1815 for cell in cleared_spill_cells {
1816 self.spill_projection_history.entry(cell).or_insert(true);
1817 }
1818 self.shift_spill_history(sheet_id, before0, count, true);
1819 if let Some(undo_ptr) = self.arrow_undo {
1820 unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
1821 }
1822 self.engine
1823 .purge_derived_formats_after_row(sheet_id, before0);
1824 self.engine
1825 .shift_row_visibility_insert(sheet_id, before0, count);
1826 self.engine.mark_moved_formula_vertices_dirty(&summary);
1827 if let Some(undo_ptr) = self.arrow_undo {
1828 self.engine.journal_structural_computed_clear(
1829 sheet,
1830 before0 as usize,
1831 true,
1832 unsafe { &mut *undo_ptr },
1833 );
1834 }
1835 self.engine
1836 .clear_computed_overlay_after_row(sheet, before0 as usize);
1837 self.engine.restore_shifted_spill_tombstones(
1838 spill_masks,
1839 before0,
1840 count,
1841 true,
1842 true,
1843 self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1844 );
1845 self.engine
1846 .record_structural_change(StructuralScope::Region(affected_region));
1847 Ok(summary)
1848 } else {
1849 self.engine.insert_rows(sheet, before, count)
1850 }
1851 }
1852
1853 #[inline]
1854 pub fn delete_rows(
1855 &mut self,
1856 sheet: &str,
1857 start: u32,
1858 count: u32,
1859 ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1860 self.engine.ensure_not_poisoned()?;
1861 if count == 0 {
1862 return Ok(crate::engine::ShiftSummary::default());
1863 }
1864 if self.atomic_policy {
1865 return Err(crate::engine::EditorError::TransactionUnsupported {
1866 reason:
1867 "delete_rows is not supported inside atomic actions (conservative rollback policy)"
1868 .to_string(),
1869 });
1870 }
1871 self.engine.delete_rows(sheet, start, count)
1872 }
1873
1874 #[inline]
1875 pub fn insert_columns(
1876 &mut self,
1877 sheet: &str,
1878 before: u32,
1879 count: u32,
1880 ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1881 self.engine.ensure_not_poisoned()?;
1882 if count == 0 {
1883 return Ok(crate::engine::ShiftSummary::default());
1884 }
1885 if self.capture.is_some() {
1886 let Some(capture_ptr) = self.capture else {
1887 return Err(crate::engine::EditorError::TransactionFailed {
1888 reason: "action_atomic: missing mutation capture".to_string(),
1889 });
1890 };
1891
1892 let before0 = before.saturating_sub(1);
1893 self.engine
1894 .check_insert_room(sheet, before0, count, false)?;
1895 let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1896 let occupancy = self.engine.structural_column_occupancy();
1897 let affected_region = Engine::<R>::structural_col_region(sheet_id, before0);
1898 let mut spill_masks = self.spill_history_masks_before_insert(sheet_id, before0, false);
1899
1900 let cleared_spill_cells = self.engine.clear_spills_before_structural_edit(
1903 sheet_id,
1904 before0,
1905 false,
1906 Some(unsafe { &mut *capture_ptr }),
1907 self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1908 )?;
1909 spill_masks.extend(cleared_spill_cells.iter().copied());
1910
1911 let summary = {
1912 let capture = unsafe { &mut *capture_ptr };
1913 let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1914 Ok(crate::engine::ShiftSummary::default());
1915 self.engine.edit_with_capture(capture, |editor| {
1916 editor.set_structural_occupancy(occupancy);
1917 out = editor.insert_columns(sheet_id, before0, count);
1918 })?;
1919 out?
1920 };
1921
1922 self.engine.ensure_arrow_sheet(sheet);
1923 if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1924 asheet.insert_columns(before0 as usize, count as usize);
1925 }
1926 for cell in cleared_spill_cells {
1927 self.spill_projection_history.entry(cell).or_insert(true);
1928 }
1929 self.shift_spill_history(sheet_id, before0, count, false);
1930 if let Some(undo_ptr) = self.arrow_undo {
1931 unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
1932 }
1933 self.engine
1934 .purge_derived_formats_after_col(sheet_id, before0);
1935 self.engine.mark_moved_formula_vertices_dirty(&summary);
1936 if let Some(undo_ptr) = self.arrow_undo {
1937 self.engine.journal_structural_computed_clear(
1938 sheet,
1939 before0 as usize,
1940 false,
1941 unsafe { &mut *undo_ptr },
1942 );
1943 }
1944 self.engine
1945 .clear_computed_overlay_after_col(sheet, before0 as usize);
1946 self.engine.restore_shifted_spill_tombstones(
1947 spill_masks,
1948 before0,
1949 count,
1950 false,
1951 true,
1952 self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1953 );
1954 self.engine
1955 .record_structural_change(StructuralScope::Region(affected_region));
1956 Ok(summary)
1957 } else {
1958 self.engine.insert_columns(sheet, before, count)
1959 }
1960 }
1961
1962 #[inline]
1963 pub fn delete_columns(
1964 &mut self,
1965 sheet: &str,
1966 start: u32,
1967 count: u32,
1968 ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1969 self.engine.ensure_not_poisoned()?;
1970 if count == 0 {
1971 return Ok(crate::engine::ShiftSummary::default());
1972 }
1973 if self.atomic_policy {
1974 return Err(crate::engine::EditorError::TransactionUnsupported {
1975 reason:
1976 "delete_columns is not supported inside atomic actions (conservative rollback policy)"
1977 .to_string(),
1978 });
1979 }
1980 self.engine.delete_columns(sheet, start, count)
1981 }
1982
1983 #[inline]
1988 pub fn action<T>(
1989 &mut self,
1990 name: impl AsRef<str>,
1991 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1992 ) -> Result<T, crate::engine::EditorError> {
1993 self.engine.ensure_not_poisoned()?;
1994 self.engine.action(name, f)
1995 }
1996}
1997
1998struct ActionDepthGuard<'a, R> {
1999 engine: *mut Engine<R>,
2000 _marker: std::marker::PhantomData<&'a mut Engine<R>>,
2001}
2002
2003impl<'a, R> Drop for ActionDepthGuard<'a, R> {
2004 fn drop(&mut self) {
2005 unsafe {
2008 let e = &mut *self.engine;
2009 e.action_depth = e.action_depth.saturating_sub(1);
2010 }
2011 }
2012}
2013
2014#[derive(Default)]
2015struct SourceCache {
2016 scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
2017 tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
2018 external_views: FxHashMap<(String, Option<u64>), RangeView<'static>>,
2021}
2022
2023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2024struct VisibilityMaskCacheKey {
2025 sheet_id: SheetId,
2026 start_row0: u32,
2027 end_row0: u32,
2028 mode: VisibilityMaskMode,
2029 version: u64,
2030}
2031
2032#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2033enum StructuralScope {
2034 Cell { sheet: SheetId, row: u32, col: u32 },
2035 Region(Region),
2036 Sheet(SheetId),
2037 RemovedSheet(SheetId),
2038 OpaqueGlobal,
2039 AllSheets,
2040}
2041
2042#[derive(Default)]
2045struct SpillBlockerWaiters {
2046 by_cell: FxHashMap<CellRef, Vec<VertexId>>,
2047 by_anchor: FxHashMap<VertexId, CellRef>,
2048}
2049
2050impl SpillBlockerWaiters {
2051 const REGION_PROBE_CELLS: u64 = 4096;
2053
2054 fn is_empty(&self) -> bool {
2055 self.by_anchor.is_empty()
2056 }
2057
2058 fn wait(&mut self, anchor: VertexId, blocker: CellRef) {
2059 match self.by_anchor.insert(anchor, blocker) {
2060 Some(old) if old == blocker => return,
2061 Some(old) => self.unlink(anchor, old),
2062 None => {}
2063 }
2064 self.by_cell.entry(blocker).or_default().push(anchor);
2065 }
2066
2067 fn forget(&mut self, anchor: VertexId) {
2068 if self.by_anchor.is_empty() {
2069 return;
2070 }
2071 if let Some(old) = self.by_anchor.remove(&anchor) {
2072 self.unlink(anchor, old);
2073 }
2074 }
2075
2076 fn unlink(&mut self, anchor: VertexId, cell: CellRef) {
2077 if let Some(waiters) = self.by_cell.get_mut(&cell) {
2078 waiters.retain(|&v| v != anchor);
2079 if waiters.is_empty() {
2080 self.by_cell.remove(&cell);
2081 }
2082 }
2083 }
2084
2085 fn take_affected(&mut self, scope: StructuralScope) -> Vec<VertexId> {
2087 let cells: Vec<CellRef> = match scope {
2088 StructuralScope::Cell { sheet, row, col } => {
2089 match Coord::try_new(row, col, true, true) {
2090 Ok(coord) if self.by_cell.contains_key(&CellRef::new(sheet, coord)) => {
2091 vec![CellRef::new(sheet, coord)]
2092 }
2093 _ => Vec::new(),
2094 }
2095 }
2096 StructuralScope::Region(region) => {
2097 let (rows, cols) = region.axis_ranges();
2098 let (r0, r1) = rows.query_bounds();
2099 let (c0, c1) = cols.query_bounds();
2100 let area = (u64::from(r1.saturating_sub(r0)) + 1)
2104 .saturating_mul(u64::from(c1.saturating_sub(c0)) + 1);
2105 if area <= Self::REGION_PROBE_CELLS {
2106 let sheet = region.sheet_id();
2107 (r0..=r1)
2110 .flat_map(|r| (c0..=c1).map(move |c| (r, c)))
2111 .filter_map(|(r, c)| Coord::try_new(r, c, true, true).ok())
2112 .map(|coord| CellRef::new(sheet, coord))
2113 .filter(|cell| self.by_cell.contains_key(cell))
2114 .collect()
2115 } else {
2116 self.by_cell
2117 .keys()
2118 .filter(|cell| {
2119 region.intersects(&Region::point(
2120 cell.sheet_id,
2121 cell.coord.row(),
2122 cell.coord.col(),
2123 ))
2124 })
2125 .copied()
2126 .collect()
2127 }
2128 }
2129 StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => self
2130 .by_cell
2131 .keys()
2132 .filter(|cell| cell.sheet_id == sheet)
2133 .copied()
2134 .collect(),
2135 StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => {
2136 self.by_cell.keys().copied().collect()
2137 }
2138 };
2139 let mut woken = Vec::new();
2140 for cell in cells {
2141 if let Some(waiters) = self.by_cell.remove(&cell) {
2142 for anchor in waiters {
2143 self.by_anchor.remove(&anchor);
2144 woken.push(anchor);
2145 }
2146 }
2147 }
2148 woken
2149 }
2150}
2151
2152#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2153enum LoggedEditImpact {
2154 NoOp,
2155 DataOnly,
2156 Topology,
2157}
2158
2159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2160enum LoggedEditDirection {
2161 Original,
2162 InverseReplay,
2163 ForwardReplay,
2164}
2165
2166#[derive(Clone, Copy)]
2167struct InvalidationBaseline {
2168 snapshot_id: u64,
2169 topology_epoch: u64,
2170}
2171
2172struct SourceCacheSession {
2173 cache: Arc<std::sync::RwLock<SourceCache>>,
2174}
2175
2176impl Drop for SourceCacheSession {
2177 fn drop(&mut self) {
2178 if let Ok(mut g) = self.cache.write() {
2179 *g = SourceCache::default();
2180 }
2181 }
2182}
2183
2184#[derive(Debug)]
2185#[non_exhaustive]
2186pub struct EvalResult {
2187 pub computed_vertices: usize,
2188 pub cycle_errors: usize,
2189 pub elapsed: std::time::Duration,
2190}
2191
2192#[derive(Clone, Debug, PartialEq, Eq)]
2193#[non_exhaustive]
2194pub struct TableMetadata {
2195 pub name: String,
2196 pub sheet: String,
2197 pub start_row: u32,
2198 pub start_col: u32,
2199 pub end_row: u32,
2200 pub end_col: u32,
2201 pub header_row: bool,
2202 pub headers: Vec<String>,
2203 pub totals_row: bool,
2204 pub empty_body: bool,
2206}
2207
2208#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2216#[non_exhaustive]
2217pub struct EngineBaselineStats {
2218 pub graph_vertex_count: usize,
2219 pub graph_formula_vertex_count: usize,
2220 pub graph_edge_count: usize,
2221 pub dirty_vertex_count: usize,
2222 pub evaluation_vertex_count: usize,
2223 pub formula_ast_root_count: usize,
2224 pub formula_ast_node_count: usize,
2225 pub staged_formula_count: usize,
2226 pub formula_plane_active_span_count: usize,
2227 pub formula_plane_producer_result_entries: usize,
2228 pub formula_plane_consumer_read_entries: usize,
2229 pub formula_plane_mixed_topology_cache_builds: u64,
2230 pub formula_plane_mixed_topology_cache_hits: u64,
2231 pub formula_plane_mixed_topology_cache_overflows: u64,
2232 pub formula_plane_dirty_pending_events: usize,
2233 pub formula_plane_dirty_region_events_recorded: u64,
2234 pub formula_plane_dirty_span_region_events_recorded: u64,
2235 pub formula_plane_dirty_whole_span_seeds_recorded: u64,
2236 pub formula_plane_dirty_global_invalidations: u64,
2237 pub formula_plane_structural_span_candidates: u64,
2238 pub formula_plane_cycle_member_span_demotions: u64,
2239 pub formula_plane_array_result_span_demotions: u64,
2240 pub retained_scc_members: usize,
2243}
2244
2245#[derive(Debug, Clone, Default)]
2246#[non_exhaustive]
2247pub struct VirtualDepTelemetry {
2248 pub candidate_vertices_total: usize,
2249 pub vdeps_vertices_total: usize,
2250 pub vdeps_edges_total: usize,
2251 pub builder_elapsed_ms_total: u128,
2252 pub schedule_virtual_passes: usize,
2253 pub schedule_static_passes: usize,
2254 pub schedule_cache_hits: usize,
2255 pub schedule_cache_misses: usize,
2256 pub reused_schedule_vertices_total: usize,
2257 pub replan_iterations: usize,
2258 pub changed_vdeps_total: usize,
2259 pub bailout_reason: Option<&'static str>,
2260 pub fallback_mode_activations: u64,
2261}
2262
2263#[derive(Debug, Clone, Default, PartialEq)]
2272#[non_exhaustive]
2273pub struct CycleTelemetry {
2274 pub static_sccs: usize,
2276 pub phantom_sccs: usize,
2278 pub live_cycles_witnessed: usize,
2280 pub circ_cells_stamped: usize,
2282 pub settle_passes_total: usize,
2285 pub max_passes_single_scc: usize,
2287 pub iterated_sccs: usize,
2290 pub converged_sccs: usize,
2293 pub capped_sccs: usize,
2299 pub max_abs_delta_at_stop: f64,
2303 pub nan_converged: usize,
2306 pub reused_sccs: usize,
2311 pub reused_scc_members: usize,
2313 pub elapsed_ms: u128,
2315}
2316
2317#[derive(Debug, Clone, Copy)]
2318struct ScheduleBuildMeta {
2319 candidate_vertices: usize,
2320 vdeps_vertices: usize,
2321 vdeps_edges: usize,
2322 builder_elapsed_ms: u128,
2323 used_virtual_schedule: bool,
2324 schedule_cache_hit: bool,
2325 schedule_cache_eligible: bool,
2326}
2327
2328#[cfg(any(test, feature = "benchmark_internal"))]
2329#[doc(hidden)]
2330#[derive(Debug, Clone, Default)]
2331pub struct RecalcReuseProbe {
2332 pub schedule_requests: usize,
2333 pub schedule_cache_hits: usize,
2334 pub schedule_cache_misses: usize,
2335 pub schedule_cache_ineligible: usize,
2336 pub schedule_builds: usize,
2337 pub schedule_base_restrictions: usize,
2339 pub schedule_shared_handles: usize,
2340 pub schedule_retained_bytes: usize,
2341 pub legacy_target_requests: usize,
2342 pub target_schedule_builds: usize,
2343 pub demand_builds: usize,
2344 pub demand_vertices: usize,
2345 pub demand_clean_formulas: usize,
2346 pub demand_explicit_edges: usize,
2347 pub demand_virtual_builder_calls: usize,
2348}
2349
2350#[cfg(any(test, feature = "benchmark_internal"))]
2351fn schedule_probe_retained_bytes(schedule: &crate::engine::Schedule) -> usize {
2352 fn vector_bytes<T>(values: &Vec<T>) -> usize {
2353 values.capacity() * std::mem::size_of::<T>()
2354 }
2355
2356 [
2357 vector_bytes(&schedule.units),
2358 vector_bytes(&schedule.layers),
2359 vector_bytes(&schedule.cycles),
2360 ]
2361 .into_iter()
2362 .chain(
2363 schedule
2364 .layers
2365 .iter()
2366 .map(|layer| vector_bytes(&layer.vertices)),
2367 )
2368 .chain(schedule.cycles.iter().map(vector_bytes))
2369 .sum()
2370}
2371
2372#[derive(Debug, Clone)]
2373struct CachedScheduleEntry {
2374 topology_epoch: u64,
2375 authority_revision: (u64, u64),
2378 candidate_vertices: VertexIdRuns,
2381 schedule: Arc<crate::engine::scheduler::Schedule>,
2382}
2383
2384#[derive(Debug, Clone, Default)]
2387struct VertexIdRuns(Vec<(u32, u32)>);
2388
2389impl VertexIdRuns {
2390 fn from_slice(ids: &[VertexId]) -> Self {
2391 let mut runs: Vec<(u32, u32)> = Vec::new();
2392 for v in ids {
2393 match runs.last_mut() {
2394 Some((first, len)) if first.checked_add(*len) == Some(v.0) => *len += 1,
2395 _ => runs.push((v.0, 1)),
2396 }
2397 }
2398 runs.shrink_to_fit();
2399 Self(runs)
2400 }
2401
2402 fn equals(&self, ids: &[VertexId]) -> bool {
2403 let mut rest = ids;
2404 for &(first, len) in &self.0 {
2405 let len = len as usize;
2406 if rest.len() < len {
2407 return false;
2408 }
2409 let (head, tail) = rest.split_at(len);
2410 if head
2411 .iter()
2412 .enumerate()
2413 .any(|(i, v)| v.0 != first.wrapping_add(i as u32))
2414 {
2415 return false;
2416 }
2417 rest = tail;
2418 }
2419 rest.is_empty()
2420 }
2421
2422 fn heap_bytes(&self) -> usize {
2423 self.0.capacity() * std::mem::size_of::<(u32, u32)>()
2424 }
2425
2426 fn len(&self) -> usize {
2428 self.0.iter().map(|&(_, len)| len as usize).sum()
2429 }
2430}
2431
2432#[cfg(test)]
2433mod vertex_id_runs_tests {
2434 use super::{VertexId, VertexIdRuns};
2435
2436 #[test]
2437 fn runs_compare_like_the_list() {
2438 let ids = |v: &[u32]| v.iter().map(|&i| VertexId(i)).collect::<Vec<_>>();
2439 let list = ids(&[5, 6, 7, 2, 3, 9, 10, 10]);
2440 let runs = VertexIdRuns::from_slice(&list);
2441 assert_eq!(runs.0, vec![(5, 3), (2, 2), (9, 2), (10, 1)]);
2442 assert!(runs.equals(&list));
2443 assert!(!runs.equals(&list[..7]));
2444 assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 11])));
2445 assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 10, 11])));
2446 assert!(VertexIdRuns::from_slice(&[]).equals(&[]));
2447 assert!(!VertexIdRuns::from_slice(&[]).equals(&list));
2448 let edge = ids(&[u32::MAX - 1, u32::MAX, 0]);
2449 assert!(VertexIdRuns::from_slice(&edge).equals(&edge));
2450 }
2451}
2452
2453#[path = "recalc_job.rs"]
2455mod job_driver;
2456pub(super) use job_driver::HeldRecalcSchedule;
2457
2458pub(super) enum EvaluationSchedule {
2459 Owned(crate::engine::scheduler::Schedule),
2460 Shared(Arc<crate::engine::scheduler::Schedule>),
2461}
2462
2463impl std::ops::Deref for EvaluationSchedule {
2464 type Target = crate::engine::scheduler::Schedule;
2465
2466 fn deref(&self) -> &Self::Target {
2467 match self {
2468 Self::Owned(schedule) => schedule,
2469 Self::Shared(schedule) => schedule,
2470 }
2471 }
2472}
2473
2474type ScheduleBuildOutput = (
2475 crate::engine::scheduler::Schedule,
2476 FxHashMap<VertexId, Vec<VertexId>>,
2477 ScheduleBuildMeta,
2478);
2479
2480type EvaluationScheduleBuildOutput = (
2481 EvaluationSchedule,
2482 FxHashMap<VertexId, Vec<VertexId>>,
2483 ScheduleBuildMeta,
2484);
2485
2486#[derive(Debug)]
2488pub struct RecalcPlan {
2489 key: RecalcPlanKey,
2490 kind: RecalcPlanKind,
2491}
2492
2493#[derive(Debug)]
2494struct RecalcPlanKey {
2495 engine_token: Arc<()>,
2496 revisions: PlanningRevisionSnapshot,
2497}
2498
2499#[derive(Clone, Debug, PartialEq, Eq)]
2500struct PlanningRevisionSnapshot {
2501 engine_topology_epoch: u64,
2502 graph_topology_revision: u64,
2503 staged: u64,
2504 symbols: u64,
2505 semantic: u64,
2506 provider: Option<u64>,
2507 deterministic_mode: crate::engine::DeterministicMode,
2508 budgets: crate::engine::EvaluationBudgets,
2509}
2510
2511#[derive(Debug)]
2512enum RecalcPlanKind {
2513 CompatibilityFull {
2514 schedule: crate::engine::Schedule,
2515 has_dynamic_refs: bool,
2516 },
2517 Target {
2518 targets: Vec<crate::engine::EvaluationTarget>,
2519 scope: crate::engine::PrepareScope,
2520 topology: RecalcTopology,
2521 dynamic_policy: DynamicPlanPolicy,
2522 },
2523}
2524
2525#[derive(Debug)]
2526enum RecalcTopology {
2527 RunLocalRecipe,
2528 Workbook,
2529}
2530
2531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2532enum DynamicPlanPolicy {
2533 BoundedTargetReplan,
2534}
2535
2536impl RecalcPlan {
2537 pub fn layer_count(&self) -> usize {
2540 match &self.kind {
2541 RecalcPlanKind::CompatibilityFull { schedule, .. } => schedule.layers.len(),
2542 RecalcPlanKind::Target { .. } => 0,
2543 }
2544 }
2545
2546 pub fn has_dynamic_refs(&self) -> bool {
2547 match &self.kind {
2548 RecalcPlanKind::CompatibilityFull {
2549 has_dynamic_refs, ..
2550 } => *has_dynamic_refs,
2551 RecalcPlanKind::Target { .. } => false,
2552 }
2553 }
2554
2555 #[cfg(test)]
2556 pub(crate) fn force_stale_reasons_for_test(
2557 &mut self,
2558 reasons: &[formualizer_common::PlanStaleReason],
2559 ) {
2560 use formualizer_common::PlanStaleReason;
2561 for reason in reasons {
2562 match reason {
2563 PlanStaleReason::Engine => {
2564 self.key.engine_token = Arc::new(());
2565 }
2566 PlanStaleReason::Provider => {
2567 self.key.revisions.provider = Some(
2568 self.key
2569 .revisions
2570 .provider
2571 .unwrap_or_default()
2572 .wrapping_add(1),
2573 );
2574 }
2575 PlanStaleReason::Semantic => {
2576 self.key.revisions.semantic = self.key.revisions.semantic.wrapping_add(1);
2577 }
2578 PlanStaleReason::Budget => {
2579 let current = self.key.revisions.budgets.work.max_work_units;
2580 self.key.revisions.budgets.work.max_work_units =
2581 Some(current.unwrap_or_default().wrapping_add(1));
2582 }
2583 PlanStaleReason::Staged => {
2584 self.key.revisions.staged = self.key.revisions.staged.wrapping_add(1);
2585 }
2586 PlanStaleReason::Symbols => {
2587 self.key.revisions.symbols = self.key.revisions.symbols.wrapping_add(1);
2588 }
2589 PlanStaleReason::Graph => {
2590 self.key.revisions.graph_topology_revision =
2591 self.key.revisions.graph_topology_revision.wrapping_add(1);
2592 }
2593 _ => {}
2594 }
2595 }
2596 }
2597}
2598
2599#[cfg(any(test, feature = "test-support"))]
2600pub(crate) mod criteria_mask_test_hooks {
2601 use std::cell::Cell;
2602
2603 thread_local! {
2604 static MASK_CALLS_ROWS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
2605 static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
2606 static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
2607 }
2608
2609 pub(crate) fn take_mask_work() -> (usize, usize) {
2610 MASK_CALLS_ROWS.with(|c| c.replace((0, 0)))
2611 }
2612
2613 pub(crate) fn note_mask(rows: usize) {
2614 MASK_CALLS_ROWS.with(|c| {
2615 let (calls, work) = c.get();
2616 c.set((calls + 1, work + rows));
2617 });
2618 }
2619
2620 pub fn reset_text_segment_counters() {
2621 TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
2622 TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
2623 }
2624
2625 pub fn text_segment_counters() -> (usize, usize) {
2626 let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
2627 let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
2628 (a, b)
2629 }
2630
2631 pub(crate) fn inc_total() {
2632 TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
2633 }
2634 pub(crate) fn inc_all_null() {
2635 TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
2636 }
2637}
2638
2639#[cfg(test)]
2640pub(crate) mod visibility_mask_test_hooks {
2641 use std::cell::Cell;
2642
2643 thread_local! {
2644 static HITS: Cell<usize> = const { Cell::new(0) };
2645 static MISSES: Cell<usize> = const { Cell::new(0) };
2646 static EVICTIONS: Cell<usize> = const { Cell::new(0) };
2647 }
2648
2649 pub fn reset() {
2650 HITS.with(|c| c.set(0));
2651 MISSES.with(|c| c.set(0));
2652 EVICTIONS.with(|c| c.set(0));
2653 }
2654
2655 pub fn counters() -> (usize, usize, usize) {
2656 let hits = HITS.with(|c| c.get());
2657 let misses = MISSES.with(|c| c.get());
2658 let evictions = EVICTIONS.with(|c| c.get());
2659 (hits, misses, evictions)
2660 }
2661
2662 pub(crate) fn inc_hit() {
2663 HITS.with(|c| c.set(c.get() + 1));
2664 }
2665
2666 pub(crate) fn inc_miss() {
2667 MISSES.with(|c| c.set(c.get() + 1));
2668 }
2669
2670 pub(crate) fn inc_eviction() {
2671 EVICTIONS.with(|c| c.set(c.get() + 1));
2672 }
2673}
2674
2675fn is_numeric_text_equality(pred: &crate::args::CriteriaPredicate) -> bool {
2676 match pred {
2677 crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(text)) => {
2678 text.trim().parse::<f64>().is_ok_and(f64::is_finite)
2679 }
2680 _ => false,
2681 }
2682}
2683
2684fn fill_numeric_equality_nulls(
2689 view: &RangeView<'_>,
2690 row_start: usize,
2691 row_len: usize,
2692 col_in_view: usize,
2693 mask: &arrow_array::BooleanArray,
2694 n: f64,
2695 ne: bool,
2696) -> Option<arrow_array::BooleanArray> {
2697 use arrow_array::Array as _;
2698 let texts = view.slice_lowered_text(row_start, row_len);
2699 let text = texts.get(col_in_view).and_then(|t| t.as_ref());
2700 match text {
2701 None => {
2702 if !ne {
2703 return Some(mask.clone());
2705 }
2706 let nulls = arrow::compute::is_null(mask).ok()?;
2707 crate::compute_prelude::boolean::or_kleene(mask, &nulls).ok()
2708 }
2709 Some(text) => {
2710 if text.len() != mask.len() {
2711 return None;
2712 }
2713 let mut out = arrow_array::builder::BooleanBuilder::with_capacity(mask.len());
2714 for i in 0..mask.len() {
2715 if mask.is_valid(i) {
2716 out.append_value(mask.value(i));
2717 } else if text.is_valid(i) {
2718 let equal = crate::locale::Locale::invariant()
2719 .parse_number_invariant(text.value(i))
2720 .is_some_and(|x| (x - n).abs() < 1e-12);
2721 out.append_value(equal != ne);
2722 } else {
2723 out.append_value(ne);
2724 }
2725 }
2726 Some(out.finish())
2727 }
2728 }
2729}
2730
2731fn compute_criteria_mask(
2732 view: &RangeView<'_>,
2733 col_in_view: usize,
2734 pred: &crate::args::CriteriaPredicate,
2735) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2736 use crate::compute_prelude::{boolean, cmp, concat_arrays};
2737 use arrow::compute::kernels::comparison::{ilike, nilike};
2738 use arrow_array::{
2739 Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
2740 };
2741
2742 fn apply_numeric_pred(
2744 chunk: &Float64Array,
2745 pred: &crate::args::CriteriaPredicate,
2746 ) -> Option<BooleanArray> {
2747 match pred {
2748 crate::args::CriteriaPredicate::Gt(n) => {
2749 cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
2750 }
2751 crate::args::CriteriaPredicate::Ge(n) => {
2752 cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2753 }
2754 crate::args::CriteriaPredicate::Lt(n) => {
2755 cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
2756 }
2757 crate::args::CriteriaPredicate::Le(n) => {
2758 cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2759 }
2760 crate::args::CriteriaPredicate::Eq(v) => match v {
2761 formualizer_common::LiteralValue::Number(x) => {
2762 cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
2763 }
2764 formualizer_common::LiteralValue::Int(i) => {
2765 cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2766 }
2767 _ => None,
2768 },
2769 crate::args::CriteriaPredicate::Ne(v) => match v {
2770 formualizer_common::LiteralValue::Number(x) => {
2771 cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
2772 }
2773 formualizer_common::LiteralValue::Int(i) => {
2774 cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2775 }
2776 _ => None,
2777 },
2778 _ => None,
2779 }
2780 }
2781
2782 let is_numeric_pred = matches!(
2784 pred,
2785 crate::args::CriteriaPredicate::Gt(_)
2786 | crate::args::CriteriaPredicate::Ge(_)
2787 | crate::args::CriteriaPredicate::Lt(_)
2788 | crate::args::CriteriaPredicate::Le(_)
2789 | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
2790 | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
2791 | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
2792 | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
2793 );
2794
2795 if is_numeric_pred {
2799 let mut bool_parts: Vec<BooleanArray> = Vec::new();
2800 let number = |v: &formualizer_common::LiteralValue| match v {
2804 formualizer_common::LiteralValue::Number(n) => Some(*n),
2805 formualizer_common::LiteralValue::Int(i) => Some(*i as f64),
2806 _ => None,
2807 };
2808 let equality = match pred {
2809 crate::args::CriteriaPredicate::Eq(v) => number(v).map(|n| (n, false)),
2810 crate::args::CriteriaPredicate::Ne(v) => number(v).map(|n| (n, true)),
2811 _ => None,
2812 };
2813 for res in view.numbers_slices() {
2814 let (rs, rl, cols_seg) = res.ok()?;
2815 if col_in_view < cols_seg.len() {
2816 let chunk = cols_seg[col_in_view].as_ref();
2817 let mut mask = apply_numeric_pred(chunk, pred)?;
2818 if let Some((n, ne)) = equality
2819 && mask.null_count() > 0
2820 {
2821 mask = fill_numeric_equality_nulls(view, rs, rl, col_in_view, &mask, n, ne)?;
2822 }
2823 bool_parts.push(mask);
2824 }
2825 }
2826
2827 if bool_parts.is_empty() {
2828 return None;
2829 } else if bool_parts.len() == 1 {
2830 return Some(std::sync::Arc::new(bool_parts.remove(0)));
2831 } else {
2832 let anys: Vec<&dyn arrow_array::Array> = bool_parts
2834 .iter()
2835 .map(|a| a as &dyn arrow_array::Array)
2836 .collect();
2837 let conc: ArrayRef = concat_arrays(&anys).ok()?;
2838 let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2839 return Some(std::sync::Arc::new(ba));
2840 }
2841 }
2842
2843 if is_numeric_text_equality(pred)
2847 || matches!(pred, crate::args::CriteriaPredicate::TextLike { .. })
2848 {
2849 for tags in view.type_tags_slices() {
2850 let (_, _, cols) = tags.ok()?;
2851 let tags = cols.get(col_in_view)?;
2852 if tags.values().iter().any(|tag| {
2853 *tag == crate::arrow_store::TypeTag::Empty as u8
2854 || *tag == crate::arrow_store::TypeTag::Number as u8
2855 || *tag == crate::arrow_store::TypeTag::Boolean as u8
2856 }) {
2857 let mut mask = BooleanBuilder::new();
2858 for chunk in view.iter_row_chunks() {
2859 let chunk = chunk.ok()?;
2860 for row in chunk.row_start..chunk.row_start + chunk.row_len {
2861 mask.append_value(crate::builtins::criteria_match(
2862 pred,
2863 &view.get_cell(row, col_in_view),
2864 ));
2865 }
2866 }
2867 return Some(std::sync::Arc::new(mask.finish()));
2868 }
2869 }
2870 }
2871
2872 if matches!(pred, crate::args::CriteriaPredicate::TextLike { pattern, .. }
2874 if pattern.contains('~'))
2875 {
2876 return None;
2877 }
2878
2879 if matches!(
2883 pred,
2884 crate::args::CriteriaPredicate::IsBlank | crate::args::CriteriaPredicate::NotBlank
2885 ) {
2886 let want_blank = matches!(pred, crate::args::CriteriaPredicate::IsBlank);
2887 let mut bool_parts: Vec<BooleanArray> = Vec::new();
2888 for tags in view.type_tags_slices() {
2889 let (_, _, cols) = tags.ok()?;
2890 let tags = cols.get(col_in_view)?;
2891 let mut bb = BooleanBuilder::with_capacity(tags.len());
2892 for i in 0..tags.len() {
2893 let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8;
2894 bb.append_value(blank == want_blank);
2895 }
2896 bool_parts.push(bb.finish());
2897 }
2898 return match bool_parts.len() {
2899 0 => None,
2900 1 => Some(std::sync::Arc::new(bool_parts.remove(0))),
2901 _ => {
2902 let anys: Vec<&dyn arrow_array::Array> = bool_parts
2903 .iter()
2904 .map(|a| a as &dyn arrow_array::Array)
2905 .collect();
2906 let conc: ArrayRef = concat_arrays(&anys).ok()?;
2907 let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2908 Some(std::sync::Arc::new(ba))
2909 }
2910 };
2911 }
2912
2913 let like_pattern = |text: &str, wildcards: bool| {
2915 let mut pattern = String::with_capacity(text.len());
2916 for ch in text.chars() {
2917 match ch {
2918 '%' | '_' | '\\' => {
2919 pattern.push('\\');
2920 pattern.push(ch);
2921 }
2922 '*' if wildcards => pattern.push('%'),
2923 '?' if wildcards => pattern.push('_'),
2924 _ => pattern.push(ch),
2925 }
2926 }
2927 pattern
2928 };
2929 let (text_kind, text_pat, empty_special) = match pred {
2930 crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
2931 (0u8, like_pattern(&t.to_lowercase(), false), t.is_empty())
2932 }
2933 crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
2934 (1u8, like_pattern(&t.to_lowercase(), false), false)
2935 }
2936 crate::args::CriteriaPredicate::TextLike {
2937 pattern,
2938 case_insensitive,
2939 } => {
2940 let p = if *case_insensitive {
2941 pattern.to_lowercase()
2942 } else {
2943 pattern.clone()
2944 };
2945 (2u8, like_pattern(&p, true), false)
2946 }
2947 _ => return None,
2948 };
2949
2950 let text_pat_is_empty = text_pat.is_empty();
2951 let ne_matches_blank = text_kind == 1 && !text_pat_is_empty;
2952 let pat = StringArray::new_scalar(text_pat);
2953 let mut bool_parts: Vec<BooleanArray> = Vec::new();
2954
2955 let mut tag_slices = view.type_tags_slices();
2956 for res in view.iter_row_chunks() {
2957 let cs = res.ok()?;
2958 if cs.row_len == 0 {
2959 continue;
2960 }
2961 #[cfg(test)]
2962 criteria_mask_test_hooks::inc_total();
2963
2964 let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
2965 if col_in_view >= slices.len() {
2966 return None;
2967 }
2968
2969 let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
2970 if empty_special || (text_kind == 1 && text_pat_is_empty) {
2971 let (tag_start, tag_len, tags) = tag_slices.next()?.ok()?;
2972 if tag_start != cs.row_start || tag_len != cs.row_len {
2973 return None;
2974 }
2975 let tags = tags.get(col_in_view)?;
2976 let strings = seg_opt.and_then(|a| a.as_any().downcast_ref::<StringArray>());
2980 let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2981 for i in 0..cs.row_len {
2982 let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8
2983 || (tags.value(i) == crate::arrow_store::TypeTag::Text as u8
2984 && strings.is_some_and(|s| s.is_valid(i) && s.value(i).is_empty()));
2985 bb.append_value(if text_kind == 0 { blank } else { !blank });
2986 }
2987 #[cfg(test)]
2988 if seg_opt.is_none() {
2989 criteria_mask_test_hooks::inc_all_null();
2990 }
2991 bool_parts.push(bb.finish());
2992 continue;
2993 }
2994 let seg = match seg_opt {
2995 Some(s) => s,
2996 None => {
2997 #[cfg(test)]
2998 criteria_mask_test_hooks::inc_all_null();
2999 if (text_kind == 0 && empty_special) || ne_matches_blank {
3000 let mut bb = BooleanBuilder::with_capacity(cs.row_len);
3002 bb.append_n(cs.row_len, true);
3003 bool_parts.push(bb.finish());
3004 } else {
3005 bool_parts.push(BooleanArray::new_null(cs.row_len));
3007 }
3008 continue;
3009 }
3010 };
3011
3012 let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
3013 let mut m = match text_kind {
3014 0 => ilike(seg_sa, &pat).ok()?,
3015 1 => nilike(seg_sa, &pat).ok()?,
3016 2 => ilike(seg_sa, &pat).ok()?,
3017 _ => return None,
3018 };
3019
3020 if ((text_kind == 0 && empty_special) || ne_matches_blank) && seg_sa.null_count() > 0 {
3026 let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
3028 for i in 0..seg_sa.len() {
3029 bb.append_value(seg_sa.is_null(i));
3030 }
3031 let nulls = bb.finish();
3032 m = boolean::or_kleene(&m, &nulls).ok()?;
3033 }
3034
3035 bool_parts.push(m);
3036 }
3037
3038 if bool_parts.is_empty() {
3039 None
3040 } else if bool_parts.len() == 1 {
3041 Some(std::sync::Arc::new(bool_parts.remove(0)))
3042 } else {
3043 let anys: Vec<&dyn arrow_array::Array> = bool_parts
3044 .iter()
3045 .map(|a| a as &dyn arrow_array::Array)
3046 .collect();
3047 let conc: ArrayRef = concat_arrays(&anys).ok()?;
3048 let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
3049 Some(std::sync::Arc::new(ba))
3050 }
3051}
3052
3053#[derive(Debug, Clone)]
3054pub struct LayerInfo {
3055 pub vertex_count: usize,
3056 pub parallel_eligible: bool,
3057 pub sample_cells: Vec<String>, }
3059
3060#[derive(Debug, Clone)]
3061pub struct EvalPlan {
3062 pub total_vertices_to_evaluate: usize,
3063 pub layers: Vec<LayerInfo>,
3064 pub cycles_detected: usize,
3065 pub dirty_count: usize,
3066 pub volatile_count: usize,
3067 pub parallel_enabled: bool,
3068 pub estimated_parallel_layers: usize,
3069 pub target_cells: Vec<String>,
3070}
3071
3072#[inline]
3075fn plane_mode_ignored() -> bool {
3076 true
3077}
3078
3079#[cfg(feature = "test-support")]
3082#[doc(hidden)]
3083pub fn formula_plane_mode_ignored_for_test() -> bool {
3084 plane_mode_ignored()
3085}
3086
3087impl<R> Engine<R>
3088where
3089 R: EvaluationContext,
3090{
3091 pub fn new(resolver: R, config: EvalConfig) -> Self {
3098 let resolver = crate::guarded_resolver::GuardedResolver::new(resolver);
3099 let config = if plane_mode_ignored() {
3104 EvalConfig {
3105 formula_plane_mode: FormulaPlaneMode::Off,
3106 ..config
3107 }
3108 } else {
3109 config
3110 };
3111 if let Err(msg) = config.cycle.validate() {
3112 panic!("invalid CycleConfig: {msg}");
3113 }
3114 crate::builtins::load_builtins();
3115 let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
3116 &config.evaluation_budgets,
3117 config.max_vertices,
3118 config.max_memory_mb,
3119 config.max_eval_time,
3120 );
3121
3122 let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
3123 #[cfg(feature = "system-clock")]
3124 {
3125 Arc::new(crate::timezone::SystemClock::new(
3126 crate::timezone::TimeZoneSpec::default(),
3127 ))
3128 }
3129 #[cfg(not(feature = "system-clock"))]
3130 {
3131 Arc::new(crate::timezone::FixedClock::new(
3132 chrono::DateTime::UNIX_EPOCH,
3133 crate::timezone::TimeZoneSpec::Utc,
3134 ))
3135 }
3136 });
3137
3138 let thread_pool = if config.enable_parallel {
3140 let mut builder = ThreadPoolBuilder::new();
3141 if let Some(max_threads) = config.max_threads {
3142 builder = builder.num_threads(max_threads);
3143 }
3144
3145 match builder.build() {
3146 Ok(pool) => Some(Arc::new(pool)),
3147 Err(_) => {
3148 None
3150 }
3151 }
3152 } else {
3153 None
3154 };
3155
3156 let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
3158 let function_provider_revision_seen = resolver.planning_semantic_revision();
3159 let mut engine = Self {
3160 criteria_mask_cache: Default::default(),
3161 graph: DependencyGraph::new_with_config(config.clone()),
3162 resolver,
3163 resolved_functions: Default::default(),
3164 operation_diagnostic_collector: None,
3165 resolved_sheet_names: FxHashMap::default(),
3166 resolution_sheet_revision: None,
3167 resolution_arrow_sheet_names: Vec::new(),
3168 resolved_arrow_sheets: Vec::new(),
3169 arrow_sheet_index: std::sync::OnceLock::new(),
3170 config,
3171 workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
3172 clock: crate::timezone::SnapshotClock::new(clock),
3173 thread_pool,
3174 recalc_epoch: 0,
3175 resumable: None,
3176 resumable_call: false,
3177 recalc_options: None,
3178 #[cfg(test)]
3179 resumable_stop_after_units_for_test: None,
3180 snapshot_id: std::sync::atomic::AtomicU64::new(1),
3181 source_mutation_generation: 0,
3182 topology_epoch: 0,
3183 cached_static_schedule: None,
3184 recent_schedules: Vec::new(),
3185 base_schedule: None,
3186 #[cfg(any(test, feature = "benchmark_internal"))]
3187 recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
3188 spill_mgr: ShimSpillManager::default(),
3189 arrow_sheets: SheetStore::default(),
3190 format_registry: crate::format::FormatRegistry::default(),
3191 derived_formats: Default::default(),
3192 #[cfg(test)]
3193 derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
3194 #[cfg(test)]
3195 family_members_for_test: std::sync::atomic::AtomicU64::new(0),
3196 #[cfg(test)]
3197 invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
3198 #[cfg(test)]
3199 lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
3200 #[cfg(test)]
3201 chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
3202 #[cfg(test)]
3203 lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
3204 #[cfg(test)]
3205 criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
3206 #[cfg(test)]
3207 memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
3208 compressed_at_build: None,
3209 #[cfg(test)]
3210 computed_overlay_set_explicit_entry_operations_for_test: 0,
3211 #[cfg(test)]
3212 computed_overlay_stale_clear_range_effects_for_test: 0,
3213 #[cfg(test)]
3214 computed_overlay_stale_clear_offset_attempts_for_test: 0,
3215 #[cfg(test)]
3216 computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
3217 has_edited: false,
3218 overlay_compactions: 0,
3219 computed_overlay_bytes_estimate: 0,
3220 computed_overlay_mirroring_disabled: false,
3221 force_materialize_range_views: false,
3222 row_bounds_cache: std::sync::RwLock::new(None),
3223 used_axis_bounds_cache: std::sync::RwLock::new(None),
3224 lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
3225 source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
3226 source_formula_token: Arc::new(()),
3227 source_family_mode: crate::engine::SourceFamilyMode::from_env(),
3228 source_family_counters: crate::engine::SourceFamilyCounters::default(),
3229 recalc_plan_token: Arc::new(()),
3230 staged_formulas: std::collections::HashMap::new(),
3231 staged_formula_index: StagedFormulaIndex::default(),
3232 blocked_pending_spills: Vec::new(),
3233 spill_blocker_waiters: SpillBlockerWaiters::default(),
3234 row_visibility: FxHashMap::default(),
3235 row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
3236 formula_parse_diagnostics: Vec::new(),
3237 last_formula_ingest_report: None,
3238 formula_ingest_report_total: FormulaIngestReport::default(),
3239 active_cancel_flag: None,
3240 active_evaluation_deadline: None,
3241 action_depth: 0,
3242 last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
3243 virtual_dep_fallback_activations: 0,
3244 last_cycle_telemetry: CycleTelemetry::default(),
3245 next_evaluation_resource_request_id: 1,
3246 poisoned: Arc::new(std::sync::atomic::AtomicBool::new(false)),
3247 #[cfg(test)]
3248 panic_internal_once: false,
3249 #[cfg(test)]
3250 panic_kernel_after_member: std::sync::atomic::AtomicUsize::new(usize::MAX),
3251 evaluation_resource_request_depth: 0,
3252 active_evaluation_resource_request: None,
3253 last_evaluation_resource_request: None,
3254 evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
3255 evaluation_resource_request_started_at: None,
3256 evaluation_resource_budgets: resolved_resources.budgets,
3257 evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
3258 active_resource_ledger: None,
3259 source_cache_footprints: Vec::new(),
3260 source_cache_accounted: 0,
3261 pending_iterative_redirty: Vec::new(),
3262 snapshot_evaluation_active: false,
3263 snapshot_volatile_redirty_pending: false,
3264 snapshot_iterative_redirty_pending: Vec::new(),
3265 retained_scc_members: FxHashMap::default(),
3266 next_retained_scc_id: 0,
3267 retained_scc_config_fingerprint: 0,
3268 retained_scc_function_epoch_seen: 0,
3269 retained_scc_provider_revision_seen: None,
3270 retained_scc_dirty_at_begin: Vec::new(),
3271 iterative_state_values: FxHashMap::default(),
3272 function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
3273 function_provider_revision_seen,
3274 #[cfg(feature = "tracing")]
3275 trace_evaluation_counters: TraceEvaluationCounters::default(),
3276 #[cfg(test)]
3277 evaluation_request_begin_count_for_test: 0,
3278 #[cfg(any(test, feature = "test-support"))]
3279 before_prepared_span_commit_hook: None,
3280 #[cfg(test)]
3281 before_target_preparation_commit_hook: None,
3282 #[cfg(test)]
3283 before_target_planning_snapshot_hook: None,
3284 #[cfg(test)]
3285 inject_target_semantic_stale_once_for_test: false,
3286 #[cfg(test)]
3287 force_virtual_dep_changes_remaining_for_test: 0,
3288 freshness: Default::default(),
3289 #[cfg(test)]
3290 fail_evaluation_commit_preflight_once_for_test: false,
3291 #[cfg(test)]
3292 target_preparation_fault_for_test: None,
3293 #[cfg(test)]
3294 force_non_cycle_schedule_fallback_for_test: false,
3295 #[cfg(test)]
3296 before_legacy_fallback_final_provider_sample_hook: None,
3297 #[cfg(test)]
3298 after_eager_proposal_commit_hook: None,
3299 };
3300 engine.config.arrow_storage_enabled = true;
3302 engine.config.delta_overlay_enabled = true;
3303 engine.config.write_formula_overlay_enabled = true;
3304 let default_sheet = engine.graph.default_sheet_name().to_string();
3305 engine.ensure_arrow_sheet(&default_sheet);
3306 engine
3307 }
3308
3309 pub fn with_thread_pool(
3314 resolver: R,
3315 config: EvalConfig,
3316 thread_pool: Arc<rayon::ThreadPool>,
3317 ) -> Self {
3318 let resolver = crate::guarded_resolver::GuardedResolver::new(resolver);
3319 if let Err(msg) = config.cycle.validate() {
3320 panic!("invalid CycleConfig: {msg}");
3321 }
3322 crate::builtins::load_builtins();
3323 let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
3324 &config.evaluation_budgets,
3325 config.max_vertices,
3326 config.max_memory_mb,
3327 config.max_eval_time,
3328 );
3329 let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
3330 #[cfg(feature = "system-clock")]
3331 {
3332 Arc::new(crate::timezone::SystemClock::new(
3333 crate::timezone::TimeZoneSpec::default(),
3334 ))
3335 }
3336 #[cfg(not(feature = "system-clock"))]
3337 {
3338 Arc::new(crate::timezone::FixedClock::new(
3339 chrono::DateTime::UNIX_EPOCH,
3340 crate::timezone::TimeZoneSpec::Utc,
3341 ))
3342 }
3343 });
3344 let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
3346 let function_provider_revision_seen = resolver.planning_semantic_revision();
3347 let mut engine = Self {
3348 criteria_mask_cache: Default::default(),
3349 graph: DependencyGraph::new_with_config(config.clone()),
3350 resolver,
3351 resolved_functions: Default::default(),
3352 operation_diagnostic_collector: None,
3353 resolved_sheet_names: FxHashMap::default(),
3354 resolution_sheet_revision: None,
3355 resolution_arrow_sheet_names: Vec::new(),
3356 resolved_arrow_sheets: Vec::new(),
3357 arrow_sheet_index: std::sync::OnceLock::new(),
3358 config,
3359 workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
3360 clock: crate::timezone::SnapshotClock::new(clock),
3361 thread_pool: Some(thread_pool),
3362 recalc_epoch: 0,
3363 resumable: None,
3364 resumable_call: false,
3365 recalc_options: None,
3366 #[cfg(test)]
3367 resumable_stop_after_units_for_test: None,
3368 snapshot_id: std::sync::atomic::AtomicU64::new(1),
3369 source_mutation_generation: 0,
3370 topology_epoch: 0,
3371 cached_static_schedule: None,
3372 recent_schedules: Vec::new(),
3373 base_schedule: None,
3374 #[cfg(any(test, feature = "benchmark_internal"))]
3375 recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
3376 spill_mgr: ShimSpillManager::default(),
3377 arrow_sheets: SheetStore::default(),
3378 format_registry: crate::format::FormatRegistry::default(),
3379 derived_formats: Default::default(),
3380 #[cfg(test)]
3381 derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
3382 #[cfg(test)]
3383 family_members_for_test: std::sync::atomic::AtomicU64::new(0),
3384 #[cfg(test)]
3385 invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
3386 #[cfg(test)]
3387 lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
3388 #[cfg(test)]
3389 chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
3390 #[cfg(test)]
3391 lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
3392 #[cfg(test)]
3393 criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
3394 #[cfg(test)]
3395 memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
3396 compressed_at_build: None,
3397 #[cfg(test)]
3398 computed_overlay_set_explicit_entry_operations_for_test: 0,
3399 #[cfg(test)]
3400 computed_overlay_stale_clear_range_effects_for_test: 0,
3401 #[cfg(test)]
3402 computed_overlay_stale_clear_offset_attempts_for_test: 0,
3403 #[cfg(test)]
3404 computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
3405 has_edited: false,
3406 overlay_compactions: 0,
3407 computed_overlay_bytes_estimate: 0,
3408 computed_overlay_mirroring_disabled: false,
3409 force_materialize_range_views: false,
3410 row_bounds_cache: std::sync::RwLock::new(None),
3411 used_axis_bounds_cache: std::sync::RwLock::new(None),
3412 lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
3413 source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
3414 source_formula_token: Arc::new(()),
3415 source_family_mode: crate::engine::SourceFamilyMode::from_env(),
3416 source_family_counters: crate::engine::SourceFamilyCounters::default(),
3417 recalc_plan_token: Arc::new(()),
3418 staged_formulas: std::collections::HashMap::new(),
3419 staged_formula_index: StagedFormulaIndex::default(),
3420 blocked_pending_spills: Vec::new(),
3421 spill_blocker_waiters: SpillBlockerWaiters::default(),
3422 row_visibility: FxHashMap::default(),
3423 row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
3424 formula_parse_diagnostics: Vec::new(),
3425 last_formula_ingest_report: None,
3426 formula_ingest_report_total: FormulaIngestReport::default(),
3427 active_cancel_flag: None,
3428 active_evaluation_deadline: None,
3429 action_depth: 0,
3430 last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
3431 virtual_dep_fallback_activations: 0,
3432 last_cycle_telemetry: CycleTelemetry::default(),
3433 next_evaluation_resource_request_id: 1,
3434 poisoned: Arc::new(std::sync::atomic::AtomicBool::new(false)),
3435 #[cfg(test)]
3436 panic_internal_once: false,
3437 #[cfg(test)]
3438 panic_kernel_after_member: std::sync::atomic::AtomicUsize::new(usize::MAX),
3439 evaluation_resource_request_depth: 0,
3440 active_evaluation_resource_request: None,
3441 last_evaluation_resource_request: None,
3442 evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
3443 evaluation_resource_request_started_at: None,
3444 evaluation_resource_budgets: resolved_resources.budgets,
3445 evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
3446 active_resource_ledger: None,
3447 source_cache_footprints: Vec::new(),
3448 source_cache_accounted: 0,
3449 pending_iterative_redirty: Vec::new(),
3450 snapshot_evaluation_active: false,
3451 snapshot_volatile_redirty_pending: false,
3452 snapshot_iterative_redirty_pending: Vec::new(),
3453 retained_scc_members: FxHashMap::default(),
3454 next_retained_scc_id: 0,
3455 retained_scc_config_fingerprint: 0,
3456 retained_scc_function_epoch_seen: 0,
3457 retained_scc_provider_revision_seen: None,
3458 retained_scc_dirty_at_begin: Vec::new(),
3459 iterative_state_values: FxHashMap::default(),
3460 function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
3461 function_provider_revision_seen,
3462 #[cfg(feature = "tracing")]
3463 trace_evaluation_counters: TraceEvaluationCounters::default(),
3464 #[cfg(test)]
3465 evaluation_request_begin_count_for_test: 0,
3466 #[cfg(any(test, feature = "test-support"))]
3467 before_prepared_span_commit_hook: None,
3468 #[cfg(test)]
3469 before_target_preparation_commit_hook: None,
3470 #[cfg(test)]
3471 before_target_planning_snapshot_hook: None,
3472 #[cfg(test)]
3473 inject_target_semantic_stale_once_for_test: false,
3474 #[cfg(test)]
3475 force_virtual_dep_changes_remaining_for_test: 0,
3476 freshness: Default::default(),
3477 #[cfg(test)]
3478 fail_evaluation_commit_preflight_once_for_test: false,
3479 #[cfg(test)]
3480 target_preparation_fault_for_test: None,
3481 #[cfg(test)]
3482 force_non_cycle_schedule_fallback_for_test: false,
3483 #[cfg(test)]
3484 before_legacy_fallback_final_provider_sample_hook: None,
3485 #[cfg(test)]
3486 after_eager_proposal_commit_hook: None,
3487 };
3488 engine.config.arrow_storage_enabled = true;
3490 engine.config.delta_overlay_enabled = true;
3491 engine.config.write_formula_overlay_enabled = true;
3492 let default_sheet = engine.graph.default_sheet_name().to_string();
3493 engine.ensure_arrow_sheet(&default_sheet);
3494 engine
3495 }
3496
3497 pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
3498 &self.workbook_load_limits
3499 }
3500
3501 pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
3505 self.assert_not_poisoned();
3506 self.workbook_load_limits = limits;
3507 }
3508
3509 fn clear_source_cache(&self) {
3510 if let Ok(mut g) = self.source_cache.write() {
3511 *g = SourceCache::default();
3512 }
3513 }
3514
3515 pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
3516 &self.last_virtual_dep_telemetry
3517 }
3518
3519 pub fn last_cycle_telemetry(&self) -> &CycleTelemetry {
3523 &self.last_cycle_telemetry
3524 }
3525
3526 pub fn last_evaluation_resource_request_stats(
3528 &self,
3529 ) -> Option<&EvaluationResourceRequestStats> {
3530 self.last_evaluation_resource_request.as_ref()
3531 }
3532
3533 pub fn evaluation_resource_baseline_stats(&self) -> EvaluationResourceBaselineStats {
3535 self.evaluation_resource_baseline
3536 }
3537
3538 pub fn evaluation_resource_budgets(&self) -> &crate::engine::EvaluationBudgets {
3539 &self.evaluation_resource_budgets
3540 }
3541
3542 pub fn evaluation_resource_config_diagnostic(
3544 &self,
3545 ) -> Option<&crate::engine::EvaluationResourceConfigDiagnostic> {
3546 self.evaluation_resource_config_diagnostic.as_ref()
3547 }
3548
3549 pub fn reset_evaluation_resource_telemetry(&mut self) {
3554 self.assert_not_poisoned();
3555 self.evaluation_resource_baseline = EvaluationResourceBaselineStats::default();
3556 self.last_evaluation_resource_request = None;
3557 }
3558
3559 fn reconcile_source_cache_footprints(&mut self) -> Result<(), ExcelError> {
3562 self.blocked_pending_spills.retain(|&(vertex, anchor, _)| {
3563 self.graph.vertex_exists(vertex)
3564 && self.graph.get_cell_ref(vertex) == Some(anchor)
3565 && matches!(
3566 self.graph.get_vertex_kind(vertex),
3567 VertexKind::FormulaScalar | VertexKind::FormulaArray
3568 )
3569 });
3570 if self.blocked_pending_spills.is_empty() {
3571 self.blocked_pending_spills = Vec::new();
3572 }
3573 let mut bytes = (self.blocked_pending_spills.capacity()
3574 * std::mem::size_of::<(VertexId, CellRef, Region)>()) as u64;
3575 self.source_cache_footprints.retain(|weak| {
3576 let Some(footprint) = weak.upgrade() else {
3577 return false;
3578 };
3579 bytes = bytes.saturating_add(footprint.load(std::sync::atomic::Ordering::Acquire));
3580 true
3581 });
3582 if let Some(ledger) = self.active_resource_ledger.as_mut() {
3583 ledger
3584 .release_retained(self.source_cache_accounted)
3585 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3586 ledger.observe_retained(bytes);
3588 self.source_cache_accounted = bytes;
3589 ledger
3590 .reserve_retained(0)
3591 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3592 }
3593 Ok(())
3594 }
3595
3596 #[cfg(test)]
3597 pub(crate) fn request_scope_for_test(&self) -> (usize, bool, bool) {
3598 (
3599 self.evaluation_resource_request_depth,
3600 self.resumable_call,
3601 self.active_cancel_flag.is_some(),
3602 )
3603 }
3604
3605 fn duration_ns(duration: std::time::Duration) -> u64 {
3606 u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3607 }
3608
3609 fn observe_evaluation_resource_request<T>(
3610 &mut self,
3611 kind: EvaluationRequestKind,
3612 evaluate: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3613 ) -> Result<T, ExcelError> {
3614 self.ensure_not_poisoned()?;
3615 let outermost = self.evaluation_resource_request_depth == 0;
3616 let _unwind_poison =
3617 outermost.then(|| super::poison::UnwindPoisonGuard(self.poisoned.clone()));
3618 if outermost && !self.resumable_call && self.has_active_recalc_job() {
3619 return Err(Self::active_recalc_job_error());
3620 }
3621 if outermost && !self.resumable_call && self.resumable.take().is_some() {
3624 self.redirty_for_next_recalc();
3625 self.freshness_begin_request();
3626 }
3627 let continuing = self.resumable_call && self.resumable.is_some();
3629 if outermost {
3630 self.resolver
3631 .callbacks
3632 .begin(self.operation_diagnostic_collector.as_ref(), !continuing);
3633 }
3634 let _callbacks = crate::external_callbacks::enter(self.resolver.callbacks.clone());
3635 if outermost
3638 && !continuing
3639 && let Some(collector) = &self.operation_diagnostic_collector
3640 {
3641 collector.reset();
3642 }
3643 if outermost {
3644 let pending = std::mem::take(&mut self.snapshot_iterative_redirty_pending);
3645 if !pending.is_empty() {
3646 self.graph.redirty_iterative_members(&pending);
3647 }
3648 }
3649 if outermost && self.snapshot_volatile_redirty_pending {
3650 self.snapshot_volatile_redirty_pending = false;
3651 self.graph.redirty_volatiles();
3652 }
3653 #[cfg(feature = "tracing")]
3654 if outermost {
3655 self.trace_evaluation_counters = TraceEvaluationCounters::default();
3656 }
3657 #[cfg(feature = "tracing")]
3658 let request_kind = if matches!(
3659 kind,
3660 EvaluationRequestKind::Full
3661 | EvaluationRequestKind::FullWithDelta
3662 | EvaluationRequestKind::FullCancellable
3663 | EvaluationRequestKind::FullLogged
3664 ) {
3665 "full"
3666 } else {
3667 "targeted"
3668 };
3669 #[cfg(feature = "tracing")]
3670 let _request_span = outermost.then(|| {
3671 crate::engine::trace::fz_span!(
3672 tracing::Level::INFO,
3673 "evaluate",
3674 "evaluate.request",
3675 kind = request_kind,
3676 mode = ?FormulaPlaneMode::Off
3677 )
3678 });
3679 if outermost {
3680 let request_id = self.next_evaluation_resource_request_id;
3681 self.next_evaluation_resource_request_id = request_id
3682 .checked_add(1)
3683 .expect("evaluation resource request ID exhausted");
3684 self.active_evaluation_resource_request = Some(EvaluationResourceRequestStats::new(
3685 request_id,
3686 kind,
3687 FormulaPlaneMode::Off,
3688 self.staged_formula_count(),
3689 ));
3690 self.evaluation_resource_baseline.record_started(request_id);
3691 self.evaluation_resource_request_started_at = Some(crate::instant::FzInstant::now());
3692 self.source_cache_accounted = 0;
3693 let mut ledger =
3694 ResourceLedger::new(Some(request_id), self.evaluation_resource_budgets.clone());
3695 if self.resumable_call
3696 && let Some(state) = &self.resumable
3697 {
3698 ledger.resume_job(state.work_units, state.active_elapsed);
3699 }
3700 self.active_resource_ledger = Some(ledger);
3701 }
3702 self.evaluation_resource_request_depth =
3703 self.evaluation_resource_request_depth.saturating_add(1);
3704 #[cfg(test)]
3705 if outermost && std::mem::take(&mut self.panic_internal_once) {
3706 panic!("injected internal evaluation panic");
3707 }
3708 let result = if outermost {
3709 self.reconcile_source_cache_footprints()
3710 .and_then(|()| self.resource_checkpoint(0))
3711 .and_then(|()| evaluate(self))
3712 } else {
3713 evaluate(self)
3714 };
3715 let result = if let Some(error) = crate::external_callbacks::pending() {
3716 Err(error)
3717 } else {
3718 result
3719 };
3720 if outermost && result.is_err() {
3721 self.freshness_abort_pass();
3722 }
3723 self.evaluation_resource_request_depth =
3724 self.evaluation_resource_request_depth.saturating_sub(1);
3725
3726 let reconciliation = if outermost {
3727 self.reconcile_source_cache_footprints()
3728 } else {
3729 Ok(())
3730 };
3731 let result = result.and_then(|value| reconciliation.map(|()| value));
3732 if outermost && result.is_err() {
3733 self.freshness_abort_pass();
3734 self.freshness_discard_read_evidence();
3735 }
3736 if outermost {
3737 let total_ns = self
3738 .evaluation_resource_request_started_at
3739 .take()
3740 .map(|start| Self::duration_ns(start.elapsed()))
3741 .unwrap_or(0);
3742 let mut stats = self
3743 .active_evaluation_resource_request
3744 .take()
3745 .expect("outer evaluation resource request has active stats");
3746 let mut ledger = self
3747 .active_resource_ledger
3748 .take()
3749 .expect("outer evaluation resource request has active ledger");
3750 ledger.release_all_scratch();
3751 if self.resumable_call
3752 && let Some(state) = &mut self.resumable
3753 {
3754 state.work_units = ledger.snapshot().work_charged;
3755 state.active_elapsed += std::time::Duration::from_nanos(total_ns);
3756 }
3757 stats.ledger.update(ledger.snapshot());
3758 stats.outcome = match &result {
3759 Ok(_) => EvaluationRequestOutcome::Success,
3760 Err(error) if error.kind == ExcelErrorKind::Cancelled => {
3761 EvaluationRequestOutcome::Cancelled
3762 }
3763 Err(_) => EvaluationRequestOutcome::Error,
3764 };
3765 if stats.dirty_lease == FormulaDirtyLeaseOutcome::Acquired {
3766 stats.dirty_lease = if stats.outcome == EvaluationRequestOutcome::Cancelled {
3767 FormulaDirtyLeaseOutcome::RetainedOnCancellation
3768 } else {
3769 FormulaDirtyLeaseOutcome::RetainedOnError
3770 };
3771 }
3772 stats.phases.total_ns = total_ns;
3773 let attributed = stats
3774 .phases
3775 .staged_prepare_ns
3776 .saturating_add(stats.phases.topology_ns)
3777 .saturating_add(stats.phases.materialization_ns);
3778 stats.phases.evaluation_ns = total_ns.saturating_sub(attributed);
3779 self.evaluation_resource_baseline.record_finished(&stats);
3780 self.last_evaluation_resource_request = Some(stats);
3781 crate::engine::trace::fz_event!(
3782 tracing::Level::INFO,
3783 "evaluate",
3784 "evaluate.summary",
3785 computed_vertices = self.trace_evaluation_counters.computed_vertices,
3786 cycles = self.trace_evaluation_counters.cycles,
3787 cancelled = matches!(
3788 &result,
3789 Err(error) if error.kind == ExcelErrorKind::Cancelled
3790 )
3791 );
3792 }
3793 if self.resolver.callbacks.failed() && self.resumable.is_none() {
3794 self.resolved_functions = Default::default();
3795 }
3796 result
3797 }
3798
3799 pub fn set_evaluation_resource_budgets(&mut self, budgets: crate::engine::EvaluationBudgets) {
3803 self.assert_not_poisoned();
3804 self.evaluation_resource_budgets = budgets.clone();
3805 self.config.evaluation_budgets = budgets.clone();
3806 self.graph.set_evaluation_budgets(budgets);
3807 }
3808
3809 #[cfg(test)]
3810 pub(crate) fn set_evaluation_budgets_for_test(
3811 &mut self,
3812 budgets: crate::engine::EvaluationBudgets,
3813 ) {
3814 self.set_evaluation_resource_budgets(budgets);
3815 }
3816
3817 fn preflight_evaluation_commit_window(
3818 &mut self,
3819 bounded_writes: usize,
3820 ) -> Result<crate::instant::FzInstant, ExcelError> {
3821 #[cfg(test)]
3822 if std::mem::take(&mut self.fail_evaluation_commit_preflight_once_for_test) {
3823 return Err(crate::engine::ResourceLedgerError::Exhausted(
3824 formualizer_common::ResourceExhaustionDetail {
3825 reason: formualizer_common::ResourceExhaustionReason::Deadline,
3826 limit: 0,
3827 observed: 1,
3828 request_id: self
3829 .active_evaluation_resource_request
3830 .as_ref()
3831 .map(|stats| stats.request_id),
3832 },
3833 )
3834 .into_excel_error());
3835 }
3836 let estimate = std::time::Duration::from_nanos(
3837 u64::try_from(bounded_writes)
3838 .unwrap_or(u64::MAX)
3839 .saturating_mul(100),
3840 );
3841 if let Some(ledger) = self.active_resource_ledger.as_mut() {
3842 ledger
3843 .preflight_commit_window(estimate)
3844 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3845 }
3846 if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3847 stats.evaluation_commit_preflight_count =
3848 stats.evaluation_commit_preflight_count.saturating_add(1);
3849 stats.evaluation_commit_estimated_ns = stats
3850 .evaluation_commit_estimated_ns
3851 .saturating_add(Self::duration_ns(estimate));
3852 }
3853 Ok(crate::instant::FzInstant::now())
3854 }
3855
3856 fn observe_evaluation_commit_window(&mut self, started: crate::instant::FzInstant) {
3857 if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3858 stats.evaluation_commit_actual_ns = stats
3859 .evaluation_commit_actual_ns
3860 .saturating_add(Self::duration_ns(started.elapsed()));
3861 }
3862 }
3863
3864 fn live_cancellation_after_work(&self, message: &'static str) -> Result<(), ExcelError> {
3876 if self
3877 .active_cancel_flag
3878 .as_ref()
3879 .is_some_and(|cancel| cancel.is_cancelled())
3880 {
3881 return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3882 }
3883 Ok(())
3884 }
3885
3886 fn cancellation_checkpoint(&self, message: &'static str) -> Result<(), ExcelError> {
3887 if self
3888 .active_cancel_flag
3889 .as_ref()
3890 .is_some_and(|cancel| cancel.is_cancelled())
3891 {
3892 return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3893 }
3894 if self
3895 .active_evaluation_deadline
3896 .is_some_and(|deadline| Instant::now() >= deadline)
3897 {
3898 return Err(crate::engine::ResourceLedgerError::Exhausted(
3899 formualizer_common::ResourceExhaustionDetail {
3900 reason: formualizer_common::ResourceExhaustionReason::Deadline,
3901 limit: 0,
3902 observed: 1,
3903 request_id: self
3904 .active_evaluation_resource_request
3905 .as_ref()
3906 .map(|request| request.request_id),
3907 },
3908 )
3909 .into_excel_error()
3910 .with_message(message));
3911 }
3912 Ok(())
3913 }
3914
3915 fn resource_checkpoint(&mut self, work_units: u64) -> Result<(), ExcelError> {
3916 let Some(ledger) = self.active_resource_ledger.as_mut() else {
3917 return Ok(());
3918 };
3919 ledger
3920 .charge_work(work_units)
3921 .and_then(|()| ledger.checkpoint_deadline())
3922 .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3923 }
3924
3925 fn charge_bounded_work(&mut self, mut work_units: u64) -> Result<(), ExcelError> {
3926 if work_units == 0 {
3927 return self.resource_checkpoint(0);
3928 }
3929 while work_units > 0 {
3930 let chunk = work_units.min(256);
3931 self.resource_checkpoint(chunk)?;
3932 work_units -= chunk;
3933 }
3934 Ok(())
3935 }
3936
3937 fn reserve_request_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3938 if let Some(ledger) = self.active_resource_ledger.as_mut() {
3939 ledger.observe_scratch(bytes);
3942 }
3943 Ok(())
3944 }
3945
3946 fn release_request_scratch(&mut self, bytes: u64) {
3947 if let Some(ledger) = self.active_resource_ledger.as_mut() {
3948 let released = ledger.release_scratch(bytes);
3949 debug_assert!(
3950 released.is_ok(),
3951 "request scratch release exceeded the outstanding reservation"
3952 );
3953 }
3954 }
3955
3956 fn reserve_topology_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3957 self.active_resource_ledger
3958 .as_mut()
3959 .map_or(Ok(()), |ledger| ledger.reserve_schedule_discovery(bytes))
3960 .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3961 }
3962
3963 fn reserve_graph_source_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3964 self.active_resource_ledger
3965 .as_mut()
3966 .map_or(Ok(()), |ledger| ledger.reserve_graph_source(bytes))
3967 .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3968 }
3969
3970 fn with_request_scratch<T>(
3971 &mut self,
3972 bytes: u64,
3973 work: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3974 ) -> Result<T, ExcelError> {
3975 self.reserve_request_scratch(bytes)?;
3976 let result = work(self);
3977 self.release_request_scratch(bytes);
3978 result
3979 }
3980
3981 fn observe_staged_preparation(
3982 &mut self,
3983 selected: usize,
3984 retained: usize,
3985 elapsed: std::time::Duration,
3986 ) {
3987 if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3988 stats.staged_selected = stats.staged_selected.saturating_add(selected as u64);
3989 stats.staged_retained = retained as u64;
3990 stats.phases.staged_prepare_ns = stats
3991 .phases
3992 .staged_prepare_ns
3993 .saturating_add(Self::duration_ns(elapsed));
3994 }
3995 }
3996
3997 fn graph_admission_enabled(&self) -> bool {
3998 crate::engine::resource_ledger::graph_admission_enabled(&self.evaluation_resource_budgets)
3999 }
4000
4001 fn preflight_graph_admission(
4002 &mut self,
4003 usage: crate::engine::resource_ledger::GraphAdmission,
4004 ) -> Result<(), ExcelError> {
4005 let request_id = self
4006 .active_evaluation_resource_request
4007 .as_ref()
4008 .map(|stats| stats.request_id);
4009 crate::engine::resource_ledger::preflight_graph_admission(
4010 &self.evaluation_resource_budgets,
4011 usage,
4012 request_id,
4013 )
4014 .map_err(crate::engine::ResourceLedgerError::into_excel_error)
4015 }
4016
4017 fn prepared_legacy_admission(
4018 &mut self,
4019 plan: &PreparedLegacyGraphPlan,
4020 materialization_cells: u64,
4021 ) -> Result<(), ExcelError> {
4022 if !self.graph_admission_enabled() {
4023 return Ok(());
4024 }
4025 let stats = self.graph.baseline_stats();
4026 let added_edges = plan.planned_edge_count().ok_or_else(|| {
4027 ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4028 })?;
4029 let removed_edges = plan.removed_edge_count().ok_or_else(|| {
4030 ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4031 })?;
4032 self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
4033 final_vertices: stats
4034 .graph_vertex_count
4035 .checked_add(plan.new_vertex_count())
4036 .ok_or_else(|| {
4037 ExcelError::new(ExcelErrorKind::NImpl)
4038 .with_message("graph vertex count overflow")
4039 })?,
4040 final_edges: stats
4041 .graph_edge_count
4042 .checked_sub(removed_edges)
4043 .and_then(|count| count.checked_add(added_edges))
4044 .ok_or_else(|| {
4045 ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4046 })?,
4047 materialization_cells,
4048 added_vertices: plan.new_vertex_count(),
4049 added_edges,
4050 })
4051 }
4052
4053 fn observe_target_admission_failure(
4054 &mut self,
4055 reason: formualizer_common::ResourceExhaustionReason,
4056 ) {
4057 if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
4058 stats.target_admission_failure = Some(reason);
4059 }
4060 }
4061
4062 fn observe_target_preparation_report(
4063 &mut self,
4064 report: &crate::engine::PreparedTargetGraphReport,
4065 ) {
4066 let reason_bit = |reason: crate::engine::OpaqueReason| -> u64 {
4067 let index = match reason {
4068 crate::engine::OpaqueReason::DynamicReference => 0,
4069 crate::engine::OpaqueReason::RuntimeTextReference => 1,
4070 crate::engine::OpaqueReason::UnknownFunction => 2,
4071 crate::engine::OpaqueReason::UnknownCustomFunction => 3,
4072 crate::engine::OpaqueReason::UnresolvedCrossSheetBinding => 4,
4073 crate::engine::OpaqueReason::UnresolvedName => 5,
4074 crate::engine::OpaqueReason::UnresolvedTable => 6,
4075 crate::engine::OpaqueReason::FormulaName => 7,
4076 crate::engine::OpaqueReason::DeferredSourcePackage => 8,
4077 crate::engine::OpaqueReason::UnsupportedSourceSemantics => 9,
4078 crate::engine::OpaqueReason::UncertainDefaultSheetBinding => 10,
4079 };
4080 1u64 << index
4081 };
4082 if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
4083 stats.staged_selected = report.selected_staged_cells as u64;
4084 stats.staged_retained = report.retained_staged_cells as u64;
4085 stats.target_requested = report.requested_targets as u64;
4086 stats.target_normalized_regions = report.normalized_regions as u64;
4087 stats.target_scope_level = match &report.widened_scope {
4088 crate::engine::PrepareScope::Exact => 0,
4089 crate::engine::PrepareScope::Sheets(_) => 1,
4090 crate::engine::PrepareScope::Workbook => 2,
4091 };
4092 stats.target_widening_reason_bits = report
4093 .widening_reasons
4094 .iter()
4095 .copied()
4096 .fold(0, |bits, reason| bits | reason_bit(reason));
4097 stats.graph_source_scratch_estimated = report.estimated_scratch_bytes;
4098 stats.graph_source_scratch_observed = report.observed_scratch_bytes;
4099 stats.target_commit_estimated_work = report.estimated_commit_work;
4100 stats.target_commit_actual_work = report.actual_commit_work;
4101 stats.target_commit_window_ns = Self::duration_ns(report.commit_window);
4102 stats.phases.staged_prepare_ns = stats
4103 .phases
4104 .staged_prepare_ns
4105 .saturating_add(Self::duration_ns(report.commit_window));
4106 }
4107 }
4108
4109 pub(crate) fn current_resolved_functions(
4115 &self,
4116 ) -> Option<&crate::interpreter::ResolvedFunctions> {
4117 self.resolved_functions
4118 .is_current(self.graph.data_store(), &self.resolver)
4119 .then_some(&self.resolved_functions)
4120 }
4121
4122 fn refresh_resolution_snapshots(&mut self) {
4123 if self.resolver.callbacks.failed() {
4124 self.resolved_functions = Default::default();
4125 }
4126 self.resolved_functions
4127 .refresh(self.graph.data_store(), &self.resolver);
4128 let named_asts = self
4129 .graph
4130 .named_ranges_iter()
4131 .map(|(_, named)| named)
4132 .chain(self.graph.sheet_named_ranges_iter().map(|(_, named)| named))
4133 .filter_map(|named| match &named.definition {
4134 NamedDefinition::Formula { ast, .. } => Some(ast),
4135 _ => None,
4136 });
4137 self.resolved_functions.refresh_ast_functions(
4138 self.graph.data_store(),
4139 &self.resolver,
4140 named_asts,
4141 self.graph.symbol_revision(),
4142 );
4143 let revision = (
4147 self.graph.sheet_reg().revision(),
4148 self.graph.data_store().resolution_revision(),
4149 self.graph.data_store().sheet_names().count(),
4150 );
4151 let arrow_names_unchanged = self
4152 .resolution_arrow_sheet_names
4153 .iter()
4154 .map(|name| name.as_ref())
4155 .eq(self
4156 .arrow_sheets
4157 .sheets
4158 .iter()
4159 .map(|sheet| sheet.name.as_ref()));
4160 if self.resolution_sheet_revision == Some(revision) && arrow_names_unchanged {
4161 return;
4162 }
4163 self.resolution_arrow_sheet_names.clear();
4164 self.resolution_arrow_sheet_names.extend(
4165 self.arrow_sheets
4166 .sheets
4167 .iter()
4168 .map(|sheet| Arc::clone(&sheet.name)),
4169 );
4170 self.resolved_sheet_names.clear();
4171 for id in self.graph.data_store().sheet_names() {
4172 let name = self.graph.data_store().resolve_ast_string(id);
4173 if let Some(sheet_id) = self.graph.sheet_id(name) {
4174 self.resolved_sheet_names.insert(
4175 id,
4176 (
4177 sheet_id,
4178 self.arrow_sheets
4179 .sheets
4180 .iter()
4181 .position(|sheet| sheet.name.as_ref() == name),
4182 ),
4183 );
4184 }
4185 }
4186 self.resolved_arrow_sheets.clear();
4187 for (index, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4188 if let Some(id) = self.graph.sheet_id(&sheet.name) {
4189 let len = usize::from(id).saturating_add(1);
4190 if self.resolved_arrow_sheets.len() < len {
4191 self.resolved_arrow_sheets.resize(len, None);
4192 }
4193 if let Some(slot) = self.resolved_arrow_sheets.get_mut(usize::from(id)) {
4194 *slot = Some(index);
4195 }
4196 }
4197 }
4198 self.resolution_sheet_revision = Some(revision);
4199 }
4200
4201 pub fn enable_operation_diagnostics(&mut self, sample_limit: usize) {
4207 self.assert_not_poisoned();
4208 self.configure_operation_diagnostics(
4209 crate::operation_diagnostics::OperationDiagnosticOptions {
4210 sample_limit,
4211 physical_telemetry: true,
4212 ..Default::default()
4213 },
4214 );
4215 }
4216
4217 pub fn configure_operation_diagnostics(
4221 &mut self,
4222 options: crate::operation_diagnostics::OperationDiagnosticOptions,
4223 ) {
4224 self.assert_not_poisoned();
4225 self.operation_diagnostic_collector = Some(Arc::new(
4226 crate::operation_diagnostics::OperationDiagnosticCollector::with_options(options),
4227 ));
4228 }
4229
4230 pub fn disable_operation_diagnostics(&mut self) {
4234 self.assert_not_poisoned();
4235 self.operation_diagnostic_collector = None;
4236 }
4237
4238 pub fn operation_diagnostics(
4239 &self,
4240 ) -> Result<Option<crate::operation_diagnostics::OperationDiagnostics>, ExcelError> {
4241 self.operation_diagnostic_collector
4242 .as_ref()
4243 .map(|collector| collector.snapshot())
4244 .transpose()
4245 }
4246
4247 pub fn take_operation_diagnostics(
4249 &mut self,
4250 ) -> Result<Option<crate::operation_diagnostics::OperationDiagnostics>, ExcelError> {
4251 self.ensure_not_poisoned()?;
4252 self.operation_diagnostic_collector
4253 .as_mut()
4254 .map(|collector| collector.take_shared_snapshot())
4255 .transpose()
4256 }
4257
4258 pub(super) fn diagnostic_request_active(&self) -> bool {
4259 self.evaluation_resource_request_depth != 0
4260 }
4261
4262 fn begin_evaluation_request(&mut self) {
4263 self.refresh_resolution_snapshots();
4264 self.freshness_begin_request();
4265 #[cfg(test)]
4266 {
4267 self.evaluation_request_begin_count_for_test = self
4268 .evaluation_request_begin_count_for_test
4269 .saturating_add(1);
4270 }
4271 self.last_cycle_telemetry = CycleTelemetry::default();
4272 self.graph.authority_sync();
4273 self.pending_iterative_redirty.clear();
4276 self.reconcile_retained_sccs_at_request_begin();
4277 self.clock.refresh();
4281 }
4282
4283 fn redirty_for_next_recalc(&mut self) {
4292 if self.snapshot_evaluation_active {
4293 self.snapshot_volatile_redirty_pending = true;
4294 } else {
4295 self.graph.redirty_volatiles();
4296 }
4297 let pending = std::mem::take(&mut self.pending_iterative_redirty);
4298 let dirty_at_begin = std::mem::take(&mut self.retained_scc_dirty_at_begin);
4299 for (vertex, scc) in dirty_at_begin {
4300 if self.retained_scc_members.get(&vertex) == Some(&scc) {
4301 self.retained_scc_members.remove(&vertex);
4307 if !self.graph.is_dirty(vertex) {
4308 self.iterative_state_values.remove(&vertex);
4309 }
4310 }
4311 }
4312 if !self.iterative_state_values.is_empty() || !self.retained_scc_members.is_empty() {
4313 let graph = &self.graph;
4314 self.iterative_state_values
4315 .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4316 self.retained_scc_members
4317 .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4318 }
4319 for &vertex in &pending {
4324 if !self.graph.is_live_formula_vertex(vertex) {
4325 continue;
4326 }
4327 if let Some(cell) = self.graph.get_cell_ref(vertex) {
4328 let sheet_name = self.graph.sheet_name(cell.sheet_id);
4329 match self.read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1) {
4330 Some(value) if !matches!(value, LiteralValue::Empty) => {
4331 self.iterative_state_values.insert(vertex, value);
4332 }
4333 _ => {
4334 self.iterative_state_values.remove(&vertex);
4335 }
4336 }
4337 }
4338 }
4339 if !pending.is_empty() {
4340 if self.snapshot_evaluation_active {
4341 self.snapshot_iterative_redirty_pending.extend(pending);
4342 } else {
4343 self.graph.redirty_iterative_members(&pending);
4344 }
4345 }
4346 }
4347
4348 fn retained_scc_config_fingerprint(&self) -> u64 {
4353 use std::hash::{Hash, Hasher};
4354 let mut hasher = rustc_hash::FxHasher::default();
4355 let config = &self.config;
4356 std::mem::discriminant(&config.cycle.detection).hash(&mut hasher);
4357 match config.cycle.policy {
4358 CyclePolicy::Error => 0u8.hash(&mut hasher),
4359 CyclePolicy::Iterate {
4360 max_iterations,
4361 max_change,
4362 } => {
4363 1u8.hash(&mut hasher);
4364 max_iterations.hash(&mut hasher);
4365 max_change.to_bits().hash(&mut hasher);
4366 }
4367 }
4368 config.date_system.hash(&mut hasher);
4369 config.workbook_seed.hash(&mut hasher);
4370 std::mem::discriminant(&config.volatile_level).hash(&mut hasher);
4371 format!("{:?}", config.deterministic_mode).hash(&mut hasher);
4374 config.range_expansion_limit.hash(&mut hasher);
4375 config.max_open_ended_rows.hash(&mut hasher);
4376 config.max_open_ended_cols.hash(&mut hasher);
4377 hasher.finish()
4378 }
4379
4380 fn reconcile_retained_sccs_at_request_begin(&mut self) {
4386 self.retained_scc_dirty_at_begin.clear();
4387 if self.retained_scc_members.is_empty() {
4388 return;
4389 }
4390 let graph = &self.graph;
4393 self.retained_scc_members
4394 .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4395 if self.retained_scc_config_fingerprint() != self.retained_scc_config_fingerprint {
4396 let members: Vec<VertexId> = self.retained_scc_members.keys().copied().collect();
4397 self.retained_scc_members.clear();
4398 self.graph.mark_dirty_many(&members);
4399 return;
4400 }
4401 let changes =
4402 crate::function_registry::semantic_changes_since(self.retained_scc_function_epoch_seen);
4403 let global_changed = changes.epoch != self.retained_scc_function_epoch_seen;
4404 let provider_revision = self.resolver.planning_semantic_revision();
4405 let provider_changed = provider_revision != self.retained_scc_provider_revision_seen;
4406 if global_changed || provider_changed {
4407 let changed: BTreeSet<(String, String)> = changes.keys.into_iter().collect();
4408 let affected: Vec<VertexId> = self
4409 .retained_scc_members
4410 .keys()
4411 .copied()
4412 .filter(|&vertex| {
4413 let Some(ast) = self.graph.get_formula(vertex) else {
4414 return true;
4415 };
4416 (global_changed
4417 && (!changes.complete || Self::ast_uses_changed_function(&ast, &changed)))
4418 || (provider_changed && Self::ast_contains_function(&ast))
4419 })
4420 .collect();
4421 for vertex in &affected {
4422 self.retained_scc_members.remove(vertex);
4423 }
4424 if !affected.is_empty() {
4425 self.graph.mark_dirty_many(&affected);
4426 }
4427 self.retained_scc_function_epoch_seen = changes.epoch;
4428 self.retained_scc_provider_revision_seen = provider_revision;
4429 }
4430 let mut dirty_sccs: FxHashSet<u64> = FxHashSet::default();
4431 let mut all_sccs: FxHashSet<u64> = FxHashSet::default();
4432 for (&vertex, &scc) in &self.retained_scc_members {
4433 all_sccs.insert(scc);
4434 if self.graph.is_dirty(vertex) {
4435 dirty_sccs.insert(scc);
4436 self.retained_scc_dirty_at_begin.push((vertex, scc));
4437 }
4438 }
4439 let reused_members = self
4440 .retained_scc_members
4441 .values()
4442 .filter(|scc| !dirty_sccs.contains(scc))
4443 .count();
4444 let t = &mut self.last_cycle_telemetry;
4445 t.reused_sccs = all_sccs.len() - dirty_sccs.len();
4446 t.reused_scc_members = reused_members;
4447 }
4448
4449 pub fn virtual_dep_fallback_activations(&self) -> u64 {
4450 self.virtual_dep_fallback_activations
4451 }
4452
4453 #[cfg(test)]
4454 pub(crate) fn lookup_index_flights_built_for_test(&self) -> usize {
4455 self.lookup_index_cache
4456 .flights_built
4457 .load(std::sync::atomic::Ordering::Relaxed)
4458 }
4459
4460 pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
4461 self.lookup_index_cache.report()
4462 }
4463
4464 fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
4465 let start_row = view.start_row();
4466 let end_row = view.end_row();
4467 let start_col = view.start_col();
4468 let end_col = view.end_col();
4469 for row in start_row..=end_row {
4470 let Ok(row_u32) = u32::try_from(row) else {
4471 return true;
4472 };
4473 for col in start_col..=end_col {
4474 let Ok(col_u32) = u32::try_from(col) else {
4475 return true;
4476 };
4477 let cell_ref = self
4478 .graph
4479 .make_cell_ref_internal(sheet_id, row_u32, col_u32);
4480 if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
4481 && self.graph.is_volatile(vertex_id)
4482 {
4483 return true;
4484 }
4485 }
4486 }
4487 false
4488 }
4489
4490 fn build_lookup_index_impl(
4491 &self,
4492 view: &RangeView<'_>,
4493 axis: LookupAxis,
4494 ) -> Option<Arc<LookupIndex>> {
4495 let (rows, cols) = view.dims();
4496 if rows == 0 || cols == 0 {
4497 self.lookup_index_cache.note_skipped_tiny();
4498 return None;
4499 }
4500 let len = match axis {
4501 LookupAxis::ColumnInView(col) => {
4502 if col >= cols {
4503 self.lookup_index_cache.note_skipped_tiny();
4504 return None;
4505 }
4506 rows
4507 }
4508 LookupAxis::RowInView(row) => {
4509 if row >= rows {
4510 self.lookup_index_cache.note_skipped_tiny();
4511 return None;
4512 }
4513 cols
4514 }
4515 };
4516 if len < 64 {
4517 self.lookup_index_cache.note_skipped_tiny();
4518 return None;
4519 }
4520
4521 let sheet_id = self.graph.sheet_id(view.sheet_name())?;
4522 let key = LookupIndexKey {
4523 sheet_id,
4524 start_row: u32::try_from(view.start_row()).ok()?,
4525 start_col: u32::try_from(view.start_col()).ok()?,
4526 end_row: u32::try_from(view.end_row()).ok()?,
4527 end_col: u32::try_from(view.end_col()).ok()?,
4528 axis,
4529 snapshot_id: self.data_snapshot_id(),
4530 };
4531 if let Some(index) = self.lookup_index_cache.get(&key) {
4532 return Some(index);
4533 }
4534 if self
4535 .lookup_index_cache
4536 .would_exceed_cap(estimate_bytes(len, 0))
4537 {
4538 self.lookup_index_cache.note_skipped_cap();
4539 return None;
4540 }
4541 if !self.lookup_index_cache.should_build(key) {
4542 return None;
4543 }
4544 self.lookup_index_cache.single_flight(key, || {
4546 if let Some(index) = self.lookup_index_cache.recheck(&key) {
4547 return Some(index);
4548 }
4549 if self.lookup_index_cache.is_known_volatile(&key) {
4550 self.lookup_index_cache.note_skipped_volatile();
4551 return None;
4552 }
4553 if self.lookup_view_contains_volatile(view, sheet_id) {
4554 self.lookup_index_cache.note_volatile_key(key);
4555 self.lookup_index_cache.note_skipped_volatile();
4556 return None;
4557 }
4558 #[cfg(test)]
4559 self.lookup_index_cache
4560 .flights_built
4561 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4562 match LookupIndex::build(view, axis, self.config.date_system).ok()? {
4563 BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
4564 BuildOutcome::ErrorInLookupAxis => {
4565 self.lookup_index_cache.note_skipped_error();
4566 None
4567 }
4568 BuildOutcome::Degenerate => {
4569 self.lookup_index_cache.note_skipped_tiny();
4570 None
4571 }
4572 }
4573 })
4574 }
4575
4576 fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
4577 if !self.config.enable_virtual_dep_telemetry {
4578 self.last_virtual_dep_telemetry = VirtualDepTelemetry {
4579 fallback_mode_activations: self.virtual_dep_fallback_activations,
4580 ..VirtualDepTelemetry::default()
4581 };
4582 }
4583 }
4584
4585 fn source_cache_session(&self) -> SourceCacheSession {
4586 self.clear_source_cache();
4587 SourceCacheSession {
4588 cache: self.source_cache.clone(),
4589 }
4590 }
4591
4592 fn resolve_source_scalar_cached(
4593 &self,
4594 name: &str,
4595 version: Option<u64>,
4596 ) -> Result<LiteralValue, ExcelError> {
4597 let key = (name.to_string(), version);
4598 if let Ok(mut g) = self.source_cache.write() {
4599 if let Some(v) = g.scalars.get(&key) {
4600 return Ok(v.clone());
4601 }
4602
4603 let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
4604 if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4605 ExcelError::new(ExcelErrorKind::Ref)
4606 .with_message(format!("Unresolved source scalar: {name}"))
4607 } else {
4608 err
4609 }
4610 })?;
4611 g.scalars.insert(key, v.clone());
4612 Ok(v)
4613 } else {
4614 self.resolver.resolve_source_scalar(name).map_err(|err| {
4615 if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4616 ExcelError::new(ExcelErrorKind::Ref)
4617 .with_message(format!("Unresolved source scalar: {name}"))
4618 } else {
4619 err
4620 }
4621 })
4622 }
4623 }
4624
4625 fn resolve_source_table_cached(
4626 &self,
4627 name: &str,
4628 version: Option<u64>,
4629 ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
4630 let key = (name.to_string(), version);
4631 if let Ok(mut g) = self.source_cache.write() {
4632 if let Some(t) = g.tables.get(&key) {
4633 return Ok(t.clone());
4634 }
4635
4636 let t = self.resolver.resolve_source_table(name).map_err(|err| {
4637 if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4638 ExcelError::new(ExcelErrorKind::Ref)
4639 .with_message(format!("Unresolved source table: {name}"))
4640 } else {
4641 err
4642 }
4643 })?;
4644 let t: Arc<dyn crate::traits::Table> = Arc::from(t);
4645 g.tables.insert(key, t.clone());
4646 Ok(t)
4647 } else {
4648 self.resolver
4649 .resolve_source_table(name)
4650 .map_err(|err| {
4651 if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4652 ExcelError::new(ExcelErrorKind::Ref)
4653 .with_message(format!("Unresolved source table: {name}"))
4654 } else {
4655 err
4656 }
4657 })
4658 .map(Arc::from)
4659 }
4660 }
4661
4662 fn source_table_to_range_view(
4663 &self,
4664 table: &dyn crate::traits::Table,
4665 spec: &Option<formualizer_parse::parser::TableSpecifier>,
4666 ) -> Result<RangeView<'static>, ExcelError> {
4667 let result = self.source_table_to_range_view_inner(table, spec);
4668 if let Some(error) = crate::external_callbacks::pending() {
4669 Err(error)
4670 } else {
4671 result
4672 }
4673 }
4674
4675 fn source_table_to_range_view_inner(
4676 &self,
4677 table: &dyn crate::traits::Table,
4678 spec: &Option<formualizer_parse::parser::TableSpecifier>,
4679 ) -> Result<RangeView<'static>, ExcelError> {
4680 use formualizer_parse::parser::{SpecialItem, TableSpecifier};
4681
4682 let owned = match spec {
4683 Some(TableSpecifier::Column(c)) => {
4684 let c = c.trim();
4685 if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
4686 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4687 "Complex structured references not yet supported".to_string(),
4688 ));
4689 }
4690 table.get_column(c)?.materialise().into_owned()
4691 }
4692 Some(TableSpecifier::ColumnRange(start, end)) => {
4693 let cols = table.columns();
4694 let start = start.trim();
4695 let end = end.trim();
4696 let start_key = start.to_lowercase();
4697 let end_key = end.to_lowercase();
4698 let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
4699 let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
4700 if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
4701 if si > ei {
4702 std::mem::swap(&mut si, &mut ei);
4703 }
4704 let h = table.data_height();
4705 let w = ei - si + 1;
4706 let mut rows = vec![vec![LiteralValue::Empty; w]; h];
4707 for (offset, ci) in (si..=ei).enumerate() {
4708 let cname = &cols[ci];
4709 let col_range = table.get_column(cname)?;
4710 let (rh, _) = col_range.dimensions();
4711 for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
4712 row[offset] = col_range.get(r, 0)?;
4713 }
4714 }
4715 rows
4716 } else {
4717 return Err(ExcelError::new(ExcelErrorKind::Ref)
4718 .with_message("Column range refers to unknown column(s)".to_string()));
4719 }
4720 }
4721 Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
4722 | Some(TableSpecifier::Headers) => table
4723 .headers_row()
4724 .map(|r| r.materialise().into_owned())
4725 .unwrap_or_default(),
4726 Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
4727 | Some(TableSpecifier::Totals) => table
4728 .totals_row()
4729 .map(|r| r.materialise().into_owned())
4730 .unwrap_or_default(),
4731 Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
4732 table
4733 .data_body()
4734 .map(|r| r.materialise().into_owned())
4735 .unwrap_or_default()
4736 }
4737 Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
4738 let mut out: Vec<Vec<LiteralValue>> = Vec::new();
4739 if let Some(h) = table.headers_row() {
4740 out.extend(h.iter_rows());
4741 }
4742 if let Some(body) = table.data_body() {
4743 out.extend(body.iter_rows());
4744 }
4745 if let Some(tr) = table.totals_row() {
4746 out.extend(tr.iter_rows());
4747 }
4748 out
4749 }
4750 Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
4751 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4752 "@ (This Row) requires table-aware context; not yet supported".to_string(),
4753 ));
4754 }
4755 Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
4756 return Err(ExcelError::new(ExcelErrorKind::NImpl)
4757 .with_message("Complex structured references not yet supported".to_string()));
4758 }
4759 None => {
4760 return Err(ExcelError::new(ExcelErrorKind::NImpl)
4761 .with_message("Table reference without specifier is unsupported".to_string()));
4762 }
4763 };
4764
4765 Ok(RangeView::from_owned_rows(owned, self.config.date_system))
4766 }
4767
4768 pub fn default_sheet_id(&self) -> SheetId {
4769 self.graph.default_sheet_id()
4770 }
4771
4772 pub fn default_sheet_name(&self) -> &str {
4773 self.graph.default_sheet_name()
4774 }
4775
4776 pub fn set_workbook_seed(&mut self, seed: u64) {
4781 self.assert_not_poisoned();
4782 self.config.workbook_seed = seed;
4783 }
4784
4785 pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
4790 self.assert_not_poisoned();
4791 self.config.volatile_level = level;
4792 }
4793
4794 pub fn set_temporal_egress(&mut self, policy: crate::engine::TemporalEgress) {
4799 self.assert_not_poisoned();
4800 self.config.temporal_egress = policy;
4801 }
4802
4803 pub fn temporal_egress(&self) -> crate::engine::TemporalEgress {
4804 self.config.temporal_egress
4805 }
4806
4807 pub fn set_deterministic_mode(
4809 &mut self,
4810 mode: crate::engine::DeterministicMode,
4811 ) -> Result<(), ExcelError> {
4812 self.ensure_not_poisoned()?;
4813 let clock = mode.build_clock()?;
4814 self.config.deterministic_mode = mode;
4815 self.clock = crate::timezone::SnapshotClock::new(clock);
4816 Ok(())
4817 }
4818
4819 pub fn set_clock(&mut self, clock: Arc<dyn crate::timezone::ClockProvider>) {
4830 self.assert_not_poisoned();
4831 self.clock = crate::timezone::SnapshotClock::new(clock);
4832 }
4833
4834 fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
4835 self.config.deterministic_mode.validate()
4836 }
4837
4838 pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
4839 self.graph.sheet_id(name)
4840 }
4841
4842 pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
4846 self.assert_not_poisoned();
4847 self.add_sheet(name)
4848 .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
4849 }
4850
4851 pub fn sheet_name(&self, id: SheetId) -> &str {
4852 self.graph.sheet_name(id)
4853 }
4854
4855 fn arrow_sheet_position(&self, id: SheetId) -> Option<usize> {
4856 let index = self.arrow_sheet_index.get_or_init(|| {
4857 let mut index = Vec::new();
4858 for (position, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4859 if let Some(id) = self.graph.sheet_id(&sheet.name) {
4860 let needed = usize::from(id).saturating_add(1);
4861 if index.len() < needed {
4862 index.resize(needed, None);
4863 }
4864 if let Some(slot) = index.get_mut(usize::from(id)) {
4865 *slot = Some(position);
4866 }
4867 }
4868 }
4869 index
4870 });
4871 index.get(usize::from(id)).copied().flatten()
4872 }
4873
4874 pub(crate) fn arrow_sheet_by_id(&self, id: SheetId) -> Option<&crate::arrow_store::ArrowSheet> {
4876 self.arrow_sheets.sheets.get(self.arrow_sheet_position(id)?)
4877 }
4878
4879 pub(crate) fn arrow_sheet_by_id_mut(
4881 &mut self,
4882 id: SheetId,
4883 ) -> Option<&mut crate::arrow_store::ArrowSheet> {
4884 let position = self.arrow_sheet_position(id)?;
4885 self.arrow_sheets.sheets.get_mut(position)
4886 }
4887
4888 fn rebuild_arrow_sheet_index(&mut self) {
4889 self.arrow_sheet_index.take();
4890 self.resolved_arrow_sheets.clear();
4891 for (index, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4892 if let Some(id) = self.graph.sheet_id(&sheet.name) {
4893 let len = usize::from(id).saturating_add(1);
4894 if self.resolved_arrow_sheets.len() < len {
4895 self.resolved_arrow_sheets.resize(len, None);
4896 }
4897 if let Some(slot) = self.resolved_arrow_sheets.get_mut(usize::from(id)) {
4898 *slot = Some(index);
4899 }
4900 }
4901 }
4902 }
4903
4904 pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
4905 self.ensure_not_poisoned()?;
4906 let id = self.graph.add_sheet(name)?;
4907 self.ensure_arrow_sheet(name);
4908 self.mark_topology_edited();
4909 Ok(id)
4910 }
4911
4912 pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
4913 self.ensure_not_poisoned()?;
4914 let source_id = self.graph.sheet_id(source).ok_or_else(|| {
4915 ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
4916 })?;
4917 if new_name.is_empty() || new_name.len() > 255 {
4918 return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
4919 }
4920 if self.graph.sheet_id(new_name).is_some() {
4921 return Err(ExcelError::new(ExcelErrorKind::Value)
4922 .with_message(format!("Sheet '{new_name}' already exists")));
4923 }
4924 let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
4925
4926 if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
4927 let mut copied_sheet = source_sheet;
4928 copied_sheet.name = Arc::<str>::from(new_name);
4929 self.arrow_sheets.sheets.push(copied_sheet);
4930 self.rebuild_arrow_sheet_index();
4931 } else {
4932 self.ensure_arrow_sheet(new_name);
4933 }
4934
4935 let duplicated_formulas = self
4936 .graph
4937 .formula_vertices()
4938 .into_iter()
4939 .filter(|vertex| {
4940 self.graph
4941 .get_cell_ref(*vertex)
4942 .is_some_and(|cell| cell.sheet_id == new_id)
4943 })
4944 .collect::<Vec<_>>();
4945 self.graph.mark_vertices_dirty_batch(&duplicated_formulas);
4946 self.mark_topology_edited();
4947 Ok(new_id)
4948 }
4949
4950 fn ensure_arrow_sheet(&mut self, name: &str) {
4951 if self.arrow_sheets.sheet(name).is_some() {
4952 return;
4953 }
4954 self.arrow_sheets
4955 .sheets
4956 .push(crate::arrow_store::ArrowSheet {
4957 name: std::sync::Arc::<str>::from(name),
4958 date_system: self.config.date_system,
4959 columns: Vec::new(),
4960 nrows: 0,
4961 chunk_starts: Vec::new(),
4962 chunk_rows: 32 * 1024,
4963 });
4964 self.rebuild_arrow_sheet_index();
4965 }
4966
4967 pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
4968 self.ensure_not_poisoned()?;
4969 let name = self.graph.sheet_name(sheet_id).to_string();
4970 self.purge_derived_formats_for_sheet(sheet_id);
4971 self.graph.remove_sheet(sheet_id)?;
4972 self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
4973 self.rebuild_arrow_sheet_index();
4974 self.clear_all_computed_overlays();
4978 self.mark_all_formula_vertices_dirty();
4979 self.clear_staged_formulas_for_sheet(&name);
4980 if self.row_visibility.remove(&sheet_id).is_some() {
4981 self.invalidate_row_visibility_mask_cache();
4982 }
4983 self.record_structural_change(StructuralScope::RemovedSheet(sheet_id));
4984 self.mark_topology_edited();
4985 Ok(())
4986 }
4987
4988 fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
4990 if let Some(asheet) = self
4991 .arrow_sheets
4992 .sheets
4993 .iter_mut()
4994 .find(|s| s.name.as_ref() == target_name)
4995 {
4996 asheet.name = std::sync::Arc::<str>::from(new_name);
4997 return true;
4998 }
4999 false
5000 }
5001
5002 pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
5003 self.ensure_not_poisoned()?;
5004 let old_name = self.graph.sheet_name(sheet_id).to_string();
5005
5006 self.rename_sheet_in_arrow_store(&old_name, new_name);
5009
5010 match self.graph.rename_sheet(sheet_id, new_name) {
5012 Ok(_) => {
5013 self.rename_staged_formula_sheet(&old_name, new_name);
5014 let sheet_vertices: Vec<VertexId> = self
5016 .graph
5017 .grid_vertices_in_sheet(sheet_id)
5018 .map(|(id, _)| id)
5019 .collect();
5020 for v_id in sheet_vertices {
5021 self.graph.mark_vertex_dirty(v_id);
5022 }
5023 self.mark_topology_edited();
5025 Ok(())
5026 }
5027 Err(e) => {
5028 self.rename_sheet_in_arrow_store(new_name, &old_name);
5030 Err(e)
5031 }
5032 }
5033 }
5034
5035 pub fn named_ranges_iter(
5036 &self,
5037 ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
5038 self.graph.named_ranges_iter()
5039 }
5040
5041 pub fn sheet_named_ranges_iter(
5042 &self,
5043 ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
5044 self.graph.sheet_named_ranges_iter()
5045 }
5046
5047 pub fn resolve_name_entry(
5048 &self,
5049 name: &str,
5050 current_sheet: SheetId,
5051 ) -> Option<&crate::engine::named_range::NamedRange> {
5052 self.graph.resolve_name_entry(name, current_sheet)
5053 }
5054
5055 pub(crate) fn name_query_scope(
5065 &self,
5066 scope_sheet: Option<&str>,
5067 ) -> Result<NameScope, ExcelError> {
5068 match scope_sheet {
5069 None => Ok(NameScope::Workbook),
5070 Some(sheet) => self
5071 .graph
5072 .sheet_id(sheet)
5073 .map(NameScope::Sheet)
5074 .ok_or_else(|| {
5075 ExcelError::new(ExcelErrorKind::Ref)
5076 .with_message(format!("name scope sheet not found: {sheet}"))
5077 }),
5078 }
5079 }
5080
5081 fn resolve_sheet_locator(
5094 &self,
5095 locator: &crate::reference::SharedSheetLocator<'_>,
5096 context_sheet: SheetId,
5097 ) -> Result<SheetId, ExcelError> {
5098 self.graph
5099 .sheet_reg()
5100 .resolve_locator(locator, context_sheet)
5101 }
5102
5103 pub fn has_name(&self, name: &str, scope_sheet: Option<&str>) -> bool {
5104 let Ok(scope) = self.name_query_scope(scope_sheet) else {
5105 return false;
5106 };
5107 self.graph
5108 .resolve_name_entry_in_scope(name, scope)
5109 .is_some()
5110 }
5111
5112 pub fn resolved_name_value(
5116 &self,
5117 name: &str,
5118 scope_sheet: Option<&str>,
5119 ) -> Option<LiteralValue> {
5120 let scope = self.name_query_scope(scope_sheet).ok()?;
5121 let entry = self.graph.resolve_name_entry_in_scope(name, scope)?;
5122 self.graph.get_value(entry.vertex)
5123 }
5124
5125 pub fn cell_in_empty_table(&self, sheet: &str, row: u32, col: u32) -> bool {
5127 let Some(sheet_id) = self.graph.sheet_id(sheet) else {
5128 return false;
5129 };
5130 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
5131 !self.graph.empty_tables_at(cell).is_empty()
5132 }
5133
5134 pub fn table_metadata(&self, name: &str) -> Option<TableMetadata> {
5135 let entry = self.graph.resolve_table_entry(name)?;
5136 Some(TableMetadata {
5137 name: entry.name.clone(),
5138 sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
5139 start_row: entry.range.start.coord.row() + 1,
5140 start_col: entry.range.start.coord.col() + 1,
5141 end_row: entry.range.end.coord.row() + 1,
5142 end_col: entry.range.end.coord.col() + 1,
5143 header_row: entry.header_row,
5144 headers: entry.headers.clone(),
5145 totals_row: entry.totals_row,
5146 empty_body: entry.empty_body,
5147 })
5148 }
5149
5150 pub fn tables(&self) -> Vec<TableMetadata> {
5152 self.graph
5153 .table_names()
5154 .into_iter()
5155 .filter_map(|name| self.table_metadata(&name))
5156 .collect()
5157 }
5158
5159 pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
5160 let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
5161
5162 for (name, named) in self.graph.named_ranges_iter() {
5163 out.push(crate::engine::named_range::NamedRangeSnapshot {
5164 name: name.clone(),
5165 scope: NameScope::Workbook,
5166 definition: named.definition.clone(),
5167 });
5168 }
5169
5170 for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
5171 out.push(crate::engine::named_range::NamedRangeSnapshot {
5172 name: name.clone(),
5173 scope: NameScope::Sheet(*sheet_id),
5174 definition: named.definition.clone(),
5175 });
5176 }
5177
5178 out.sort_by(|a, b| {
5179 let a_scope = match a.scope {
5180 NameScope::Workbook => (0u8, 0u32),
5181 NameScope::Sheet(id) => (1u8, u32::from(id)),
5182 };
5183 let b_scope = match b.scope {
5184 NameScope::Workbook => (0u8, 0u32),
5185 NameScope::Sheet(id) => (1u8, u32::from(id)),
5186 };
5187 a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
5188 });
5189
5190 out
5191 }
5192
5193 pub fn named_ranges_snapshot_for_sheet(
5194 &self,
5195 sheet_id: SheetId,
5196 ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
5197 self.named_ranges_snapshot()
5198 .into_iter()
5199 .filter(|entry| match entry.scope {
5200 NameScope::Workbook => true,
5201 NameScope::Sheet(id) => id == sheet_id,
5202 })
5203 .collect()
5204 }
5205
5206 pub fn define_name(
5207 &mut self,
5208 name: &str,
5209 definition: NamedDefinition,
5210 scope: NameScope,
5211 ) -> Result<(), ExcelError> {
5212 self.ensure_not_poisoned()?;
5213 self.graph.validate_define_name(name, scope)?;
5214 self.graph.define_name(name, definition, scope)?;
5215 self.record_structural_change(StructuralScope::AllSheets);
5216
5217 self.mark_topology_edited();
5218
5219 Ok(())
5220 }
5221
5222 pub fn update_name(
5223 &mut self,
5224 name: &str,
5225 definition: NamedDefinition,
5226 scope: NameScope,
5227 ) -> Result<(), ExcelError> {
5228 self.ensure_not_poisoned()?;
5229 self.graph.validate_existing_name(name, scope)?;
5230 self.graph.update_name(name, definition, scope)?;
5231 self.record_structural_change(StructuralScope::AllSheets);
5232
5233 self.mark_topology_edited();
5234
5235 Ok(())
5236 }
5237
5238 pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
5239 self.ensure_not_poisoned()?;
5240 self.graph.validate_existing_name(name, scope)?;
5241 self.graph.delete_name(name, scope)?;
5242 self.record_structural_change(StructuralScope::AllSheets);
5243
5244 self.mark_topology_edited();
5245
5246 Ok(())
5247 }
5248
5249 pub fn define_table(
5250 &mut self,
5251 name: &str,
5252 range: crate::reference::RangeRef,
5253 header_row: bool,
5254 headers: Vec<String>,
5255 totals_row: bool,
5256 ) -> Result<(), ExcelError> {
5257 self.ensure_not_poisoned()?;
5258 self.graph
5259 .define_table(name, range, header_row, headers, totals_row)?;
5260 self.record_structural_change(StructuralScope::AllSheets);
5261 self.mark_topology_edited();
5262 Ok(())
5263 }
5264
5265 fn table_resize_range(&self, name: &str, new_end_row: u32) -> Result<RangeRef, ExcelError> {
5266 let entry = self.graph.resolve_table_entry(name).ok_or_else(|| {
5267 ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
5268 })?;
5269 let minimum = entry.range.start.coord.row()
5270 + 1
5271 + u32::from(entry.header_row)
5272 + u32::from(entry.totals_row);
5273 if new_end_row < minimum || new_end_row > 1_048_576 {
5274 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
5275 "Table resize must retain at least one body row within the sheet grid",
5276 ));
5277 }
5278 let mut range = entry.range;
5279 range.end.coord = Coord::from_excel(new_end_row, range.end.coord.col() + 1, true, true);
5280 Ok(range)
5281 }
5282
5283 pub fn resize_table(
5285 &mut self,
5286 name: &str,
5287 new_end_row: u32,
5288 ) -> Result<TableMetadata, ExcelError> {
5289 self.resize_table_body(name, new_end_row, false)
5290 }
5291
5292 pub fn resize_table_body(
5294 &mut self,
5295 name: &str,
5296 new_end_row: u32,
5297 empty_body: bool,
5298 ) -> Result<TableMetadata, ExcelError> {
5299 self.ensure_not_poisoned()?;
5300 let range = self.table_resize_range(name, new_end_row)?;
5301 self.graph.resize_table_body(name, range, empty_body)?;
5302 self.record_structural_change(StructuralScope::AllSheets);
5303 self.mark_topology_edited();
5304 Ok(self.table_metadata(name).expect("resized table exists"))
5305 }
5306
5307 pub fn define_source_scalar(
5308 &mut self,
5309 name: &str,
5310 version: Option<u64>,
5311 ) -> Result<(), ExcelError> {
5312 self.ensure_not_poisoned()?;
5313 self.graph.define_source_scalar(name, version)?;
5314 self.record_structural_change(StructuralScope::OpaqueGlobal);
5315 self.mark_topology_edited();
5316 Ok(())
5317 }
5318
5319 pub fn define_source_table(
5320 &mut self,
5321 name: &str,
5322 version: Option<u64>,
5323 ) -> Result<(), ExcelError> {
5324 self.ensure_not_poisoned()?;
5325 self.graph.define_source_table(name, version)?;
5326 self.record_structural_change(StructuralScope::OpaqueGlobal);
5327 self.mark_topology_edited();
5328 Ok(())
5329 }
5330
5331 pub fn set_source_scalar_version(
5332 &mut self,
5333 name: &str,
5334 version: Option<u64>,
5335 ) -> Result<(), ExcelError> {
5336 self.ensure_not_poisoned()?;
5337 self.graph.set_source_scalar_version(name, version)?;
5338 self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5339 Ok(())
5340 }
5341
5342 pub fn set_source_table_version(
5343 &mut self,
5344 name: &str,
5345 version: Option<u64>,
5346 ) -> Result<(), ExcelError> {
5347 self.ensure_not_poisoned()?;
5348 self.graph.set_source_table_version(name, version)?;
5349 self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5350 Ok(())
5351 }
5352
5353 pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
5354 self.ensure_not_poisoned()?;
5355 self.graph.invalidate_source(name)?;
5356 self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5357 Ok(())
5358 }
5359
5360 pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
5361 self.graph.get_value(vertex)
5362 }
5363
5364 pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
5365 self.graph.get_cell_value(sheet, row, col)
5366 }
5367
5368 pub fn cell_has_spill_projection(&self, sheet: &str, row: u32, col: u32) -> bool {
5373 if self.graph.spill_registry_is_empty() {
5374 return false;
5375 }
5376 let Some(sheet_id) = self.graph.sheet_id(sheet) else {
5377 return false;
5378 };
5379 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
5380 self.graph.spill_registry_anchor_for_cell(cell).is_some()
5381 || self
5382 .graph
5383 .get_vertex_for_cell(&cell)
5384 .is_some_and(|vertex| self.graph.spill_registry_has_anchor(vertex))
5385 }
5386
5387 pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
5388 self.graph.get_vertex_for_cell(cell)
5389 }
5390
5391 pub fn evaluation_vertices(&self) -> Vec<VertexId> {
5392 self.graph.get_evaluation_vertices()
5393 }
5394
5395 pub fn baseline_stats(&self) -> EngineBaselineStats {
5397 let graph = self.graph.baseline_stats();
5398 EngineBaselineStats {
5399 graph_vertex_count: graph.graph_vertex_count,
5400 graph_formula_vertex_count: graph.graph_formula_vertex_count,
5401 graph_edge_count: graph.graph_edge_count,
5402 dirty_vertex_count: graph.dirty_vertex_count,
5403 evaluation_vertex_count: graph.evaluation_vertex_count,
5404 formula_ast_root_count: graph.formula_ast_root_count,
5405 formula_ast_node_count: graph.formula_ast_node_count,
5406 staged_formula_count: self.staged_formula_count(),
5407 formula_plane_active_span_count: 0,
5408 formula_plane_producer_result_entries: 0,
5409 formula_plane_consumer_read_entries: 0,
5410 formula_plane_mixed_topology_cache_builds: 0,
5411 formula_plane_mixed_topology_cache_hits: 0,
5412 formula_plane_mixed_topology_cache_overflows: 0,
5413 formula_plane_dirty_pending_events: 0,
5414 formula_plane_dirty_region_events_recorded: 0,
5415 formula_plane_dirty_span_region_events_recorded: 0,
5416 formula_plane_dirty_whole_span_seeds_recorded: 0,
5417 formula_plane_dirty_global_invalidations: 0,
5418 formula_plane_structural_span_candidates: 0,
5419 formula_plane_cycle_member_span_demotions: 0,
5420 formula_plane_array_result_span_demotions: 0,
5421 retained_scc_members: self.retained_scc_members.len(),
5422 }
5423 }
5424
5425 pub(super) fn affected_closure_idle(&self) -> bool {
5427 self.active_evaluation_resource_request.is_none()
5428 && self.active_resource_ledger.is_none()
5429 && self.action_depth == 0
5430 && !self.freshness_armed()
5431 && !self.has_staged_formulas()
5432 && !self.graph.deferred_dirty_active()
5433 }
5434
5435 pub(super) fn affected_closure_owner(&self) -> &Arc<()> {
5436 &self.recalc_plan_token
5437 }
5438
5439 pub(crate) fn inspection_mutation_revision(&self) -> u64 {
5441 self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
5442 }
5443
5444 #[cfg(test)]
5445 pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
5446 self.used_axis_bounds_cache
5447 .read()
5448 .ok()
5449 .and_then(|guard| {
5450 guard.as_ref().map(|cache| {
5451 (
5452 cache.row_hits.load(Ordering::Relaxed),
5453 cache.row_misses.load(Ordering::Relaxed),
5454 cache.col_hits.load(Ordering::Relaxed),
5455 cache.col_misses.load(Ordering::Relaxed),
5456 )
5457 })
5458 })
5459 .unwrap_or((0, 0, 0, 0))
5460 }
5461
5462 pub fn set_first_load_assume_new(&mut self, enabled: bool) {
5466 self.assert_not_poisoned();
5467 self.graph.set_first_load_assume_new(enabled);
5468 }
5469
5470 pub fn first_load_assume_new(&self) -> bool {
5471 self.graph.first_load_assume_new()
5472 }
5473
5474 pub fn reset_ensure_touched(&mut self) {
5478 self.assert_not_poisoned();
5479 self.graph.reset_ensure_touched();
5480 }
5481
5482 pub fn finalize_sheet_index(&mut self, sheet: &str) {
5486 self.assert_not_poisoned();
5487 self.graph.finalize_sheet_index(sheet);
5488 }
5489
5490 #[doc(hidden)]
5492 pub fn finalize_all_sheet_indexes<'a>(&mut self, sheets: impl IntoIterator<Item = &'a str>) {
5496 self.assert_not_poisoned();
5497 self.graph.finalize_all_sheet_indexes(sheets);
5498 }
5499
5500 pub fn action<T>(
5509 &mut self,
5510 name: impl AsRef<str>,
5511 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5512 ) -> Result<T, crate::engine::EditorError> {
5513 self.ensure_not_poisoned()?;
5514 if self.action_depth != 0 {
5515 return Err(crate::engine::EditorError::TransactionFailed {
5516 reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
5517 .to_string(),
5518 });
5519 }
5520
5521 self.action_depth = 1;
5522 let engine_ptr: *mut Engine<R> = self;
5523 let _guard = ActionDepthGuard {
5524 engine: engine_ptr,
5525 _marker: std::marker::PhantomData,
5526 };
5527
5528 let mut tx = EngineAction {
5529 engine: self,
5530 name: name.as_ref().to_string(),
5531 capture: None,
5532 arrow_undo: None,
5533 atomic_policy: false,
5534 spill_projection_history: Default::default(),
5535 };
5536 f(&mut tx)
5537 }
5538
5539 pub fn action_atomic<T>(
5543 &mut self,
5544 name: impl Into<String>,
5545 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5546 ) -> Result<T, crate::engine::EditorError> {
5547 self.ensure_not_poisoned()?;
5548 let (v, _j) = self.action_atomic_journal(name, f)?;
5549 Ok(v)
5550 }
5551
5552 pub fn action_atomic_journal<T>(
5554 &mut self,
5555 name: impl Into<String>,
5556 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5557 ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
5558 self.ensure_not_poisoned()?;
5559 if self.action_depth != 0 {
5560 return Err(crate::engine::EditorError::TransactionFailed {
5561 reason: "Nested Engine::action calls are not supported (deterministic rule)"
5562 .to_string(),
5563 });
5564 }
5565
5566 self.action_depth = 1;
5567 let engine_ptr: *mut Engine<R> = self;
5568 let _guard = ActionDepthGuard {
5569 engine: engine_ptr,
5570 _marker: std::marker::PhantomData,
5571 };
5572
5573 let name_str = name.into();
5574 let mut capture = MutationCapture::new(Default::default());
5575 let start_len = capture.len();
5576 self.action_atomic_impl(&mut capture, start_len, true, name_str, f)
5577 }
5578
5579 fn action_atomic_impl<T>(
5580 &mut self,
5581 capture: &mut MutationCapture,
5582 start_len: usize,
5583 expand_runs: bool,
5584 name: String,
5585 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5586 ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
5587 let invalidation_baseline = self.invalidation_baseline();
5588 let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
5589 let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
5590
5591 let capture_ptr: *mut MutationCapture = capture;
5592 let mut tx = EngineAction {
5593 engine: self,
5594 name: name.clone(),
5595 capture: Some(capture_ptr),
5596 arrow_undo: Some(arrow_ptr),
5597 atomic_policy: true,
5598 spill_projection_history: Default::default(),
5599 };
5600
5601 let res = f(&mut tx);
5602
5603 let capture_ref = unsafe { &*capture_ptr };
5610 let has_runs = capture_ref.lazy_len() > 0;
5611 let graph_events: Vec<crate::engine::ChangeEvent> = if expand_runs || res.is_err() {
5612 capture_ref.expanded_events_from(start_len, 0)
5613 } else {
5614 capture_ref.events()[start_len..].to_vec()
5615 };
5616 let graph_batch = crate::engine::GraphUndoBatch {
5617 events: graph_events,
5618 };
5619 let affected_cells = arrow_undo.ops.len();
5620 let journal = crate::engine::ActionJournal {
5621 name,
5622 graph: graph_batch,
5623 arrow: arrow_undo,
5624 affected_cells,
5625 };
5626
5627 match res {
5628 Ok(v) => {
5629 if !journal.graph.is_empty() || !journal.arrow.is_empty() || has_runs {
5630 for event in &journal.graph.events {
5631 self.record_change_for_event(event);
5632 }
5633 let mut impact = Self::classify_change_events(
5634 &journal.graph.events,
5635 LoggedEditDirection::Original,
5636 )
5637 .max(Self::classify_arrow_undo(&journal.arrow));
5638 if has_runs {
5639 impact = impact.max(LoggedEditImpact::Topology);
5640 }
5641 self.apply_logged_edit_impact(impact, invalidation_baseline);
5642 }
5643 Ok((v, journal))
5644 }
5645 Err(e) => {
5646 if let Err(rb) = self.rollback_from_action_journal(&journal, invalidation_baseline)
5647 {
5648 return Err(crate::engine::EditorError::TransactionFailed {
5649 reason: format!(
5650 "Engine::action_atomic rollback failed after error '{e}': {rb}"
5651 ),
5652 });
5653 }
5654 if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5655 for event in &journal.graph.events {
5656 self.record_inverse_change_for_event(event);
5660 }
5661 }
5662 Err(e)
5663 }
5664 }
5665 }
5666
5667 pub fn action_with_logger<T>(
5674 &mut self,
5675 log: &mut crate::engine::ChangeLog,
5676 name: impl AsRef<str>,
5677 f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5678 ) -> Result<T, crate::engine::EditorError> {
5679 self.ensure_not_poisoned()?;
5680 if self.action_depth != 0 {
5681 return Err(crate::engine::EditorError::TransactionFailed {
5682 reason: "Nested Engine::action calls are not supported (deterministic rule)"
5683 .to_string(),
5684 });
5685 }
5686
5687 self.action_depth = 1;
5688 let engine_ptr: *mut Engine<R> = self;
5689 let _guard = ActionDepthGuard {
5690 engine: engine_ptr,
5691 _marker: std::marker::PhantomData,
5692 };
5693
5694 let name_str = name.as_ref().to_string();
5695 let mut capture = MutationCapture::new(log.current_meta());
5696 let start_len = capture.len();
5697 capture.begin_compound(name_str.clone());
5698
5699 let res = self.action_atomic_impl(&mut capture, start_len, false, name_str, f);
5702 capture.close_compounds();
5703
5704 match res {
5705 Ok((v, _journal)) => {
5706 log.publish_capture(capture);
5707 Ok(v)
5708 }
5709 Err(e) => {
5710 log.discard_capture(capture);
5712 Err(e)
5713 }
5714 }
5715 }
5716
5717 fn rollback_from_action_journal(
5718 &mut self,
5719 journal: &crate::engine::ActionJournal,
5720 invalidation_baseline: InvalidationBaseline,
5721 ) -> Result<(), crate::engine::EditorError> {
5722 self.invalidate_for_action_journal(
5725 journal,
5726 LoggedEditDirection::InverseReplay,
5727 invalidation_baseline,
5728 );
5729 journal.graph.undo(&mut self.graph)?;
5731 self.apply_inverse_row_visibility_events(&journal.graph.events);
5733 self.apply_arrow_undo_batch(&journal.arrow, true);
5735 Ok(())
5736 }
5737
5738 fn rollback_from_change_events(
5739 &mut self,
5740 events: &[crate::engine::ChangeEvent],
5741 invalidation_baseline: InvalidationBaseline,
5742 ) -> Result<(), crate::engine::EditorError> {
5743 use crate::engine::ChangeEvent;
5744
5745 self.invalidate_for_change_events(
5748 events,
5749 LoggedEditDirection::InverseReplay,
5750 invalidation_baseline,
5751 );
5752
5753 self.graph
5755 .authority_set_replay(crate::engine::authority::history::Replay::Undo);
5756 let rolled_back = (|| {
5757 let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
5758 let mut compound_stack: Vec<usize> = Vec::new();
5759 for (i, ev) in events.iter().enumerate().rev() {
5760 match ev {
5761 ChangeEvent::CompoundEnd { depth } => {
5762 compound_stack.push(*depth);
5763 if let Some(description) =
5764 crate::engine::graph::editor::change_log::compound_start_description(
5765 i,
5766 |j| &events[j],
5767 )
5768 {
5769 editor.inverse_compound_end(description);
5770 }
5771 }
5772 ChangeEvent::CompoundStart { depth, .. } => {
5773 if compound_stack.last() == Some(depth) {
5774 compound_stack.pop();
5775 }
5776 editor.apply_inverse(ev.clone())?;
5777 }
5778 ChangeEvent::SetRowVisibility { .. } => {
5779 }
5781 _ => {
5782 editor.apply_inverse(ev.clone())?;
5783 }
5784 }
5785 }
5786 Ok::<_, crate::engine::EditorError>(())
5787 })();
5788 self.graph
5789 .authority_set_replay(crate::engine::authority::history::Replay::Forward);
5790 rolled_back?;
5791
5792 for ev in events.iter().rev() {
5794 self.apply_inverse_row_visibility_event(ev);
5795 }
5796
5797 for ev in events.iter().rev() {
5799 self.mirror_inverse_change_to_arrow(ev);
5800 }
5801
5802 Ok(())
5803 }
5804
5805 fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
5806 let sheet_id = self.graph.sheet_id(sheet)?;
5807 let coord = Coord::from_excel(row, col, true, true);
5808 let cell = CellRef::new(sheet_id, coord);
5809 let vid = self.graph.get_vertex_for_cell(&cell)?;
5810 self.graph.get_formula(vid)
5811 }
5812
5813 pub fn define_name_with_logger(
5814 &mut self,
5815 log: &mut crate::engine::ChangeLog,
5816 name: &str,
5817 definition: NamedDefinition,
5818 scope: NameScope,
5819 ) -> Result<(), crate::engine::EditorError> {
5820 self.ensure_not_poisoned()?;
5821 self.graph
5822 .validate_define_name(name, scope)
5823 .map_err(crate::engine::EditorError::Excel)?;
5824
5825 {
5826 let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5827 editor.define_name(name, definition, scope)?;
5828 }
5829 self.record_structural_change(StructuralScope::AllSheets);
5830
5831 self.mark_topology_edited();
5832
5833 Ok(())
5834 }
5835
5836 pub fn update_name_with_logger(
5837 &mut self,
5838 log: &mut crate::engine::ChangeLog,
5839 name: &str,
5840 definition: NamedDefinition,
5841 scope: NameScope,
5842 ) -> Result<(), crate::engine::EditorError> {
5843 self.ensure_not_poisoned()?;
5844 self.graph
5845 .validate_existing_name(name, scope)
5846 .map_err(crate::engine::EditorError::Excel)?;
5847 {
5848 let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5849 editor.update_name(name, definition, scope)?;
5850 }
5851 self.record_structural_change(StructuralScope::AllSheets);
5852
5853 self.mark_topology_edited();
5854
5855 Ok(())
5856 }
5857
5858 pub fn delete_name_with_logger(
5859 &mut self,
5860 log: &mut crate::engine::ChangeLog,
5861 name: &str,
5862 scope: NameScope,
5863 ) -> Result<(), crate::engine::EditorError> {
5864 self.ensure_not_poisoned()?;
5865 self.graph
5866 .validate_existing_name(name, scope)
5867 .map_err(crate::engine::EditorError::Excel)?;
5868 {
5869 let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5870 editor.delete_name(name, scope)?;
5871 }
5872 self.record_structural_change(StructuralScope::AllSheets);
5873
5874 self.mark_topology_edited();
5875
5876 Ok(())
5877 }
5878
5879 pub fn edit_with_logger<T>(
5880 &mut self,
5881 log: &mut crate::engine::ChangeLog,
5882 f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5883 ) -> Result<T, crate::engine::EditorError> {
5884 self.ensure_not_poisoned()?;
5885 let mut capture = MutationCapture::new(log.current_meta());
5886 let result = self.edit_with_capture(&mut capture, f);
5887 capture.close_compounds();
5888 match result {
5889 Ok(value) => {
5890 log.publish_capture(capture);
5891 Ok(value)
5892 }
5893 Err(error) => {
5894 log.discard_capture(capture);
5895 Err(error)
5896 }
5897 }
5898 }
5899
5900 fn edit_with_capture<T>(
5901 &mut self,
5902 capture: &mut MutationCapture,
5903 f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5904 ) -> Result<T, crate::engine::EditorError> {
5905 let invalidation_baseline = self.invalidation_baseline();
5906 let start_len = capture.len();
5907 let lazy_start = capture.lazy_len();
5908
5909 struct ArrowSpillReader<'a> {
5912 sheets: &'a crate::arrow_store::SheetStore,
5913 }
5914 impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
5915 fn read_cell_value(
5916 &self,
5917 sheet: &str,
5918 row: u32,
5919 col: u32,
5920 ) -> Option<formualizer_common::LiteralValue> {
5921 use formualizer_common::LiteralValue;
5922 let asheet = self.sheets.sheet(sheet)?;
5923 let r0 = row.saturating_sub(1) as usize;
5924 let c0 = col.saturating_sub(1) as usize;
5925 let v = asheet.get_cell_value(r0, c0);
5926 if matches!(v, LiteralValue::Empty) {
5927 None
5928 } else {
5929 Some(v)
5930 }
5931 }
5932 }
5933
5934 let ret = {
5935 let spill_reader = ArrowSpillReader {
5936 sheets: &self.arrow_sheets,
5937 };
5938 let mut editor = crate::engine::VertexEditor::with_capture_and_spill_reader(
5939 &mut self.graph,
5940 capture,
5941 &spill_reader,
5942 );
5943 f(&mut editor)
5944 };
5945
5946 let new_events = capture.events()[start_len..].to_vec();
5950 let new_runs = capture.lazy_len() > lazy_start;
5951 if new_events.iter().any(|event| {
5952 matches!(
5953 event,
5954 ChangeEvent::DefineName { .. }
5955 | ChangeEvent::UpdateName { .. }
5956 | ChangeEvent::DeleteName { .. }
5957 )
5958 }) {
5959 let all = capture.expanded_events_from(start_len, lazy_start);
5960 self.rollback_from_change_events(&all, invalidation_baseline)?;
5961 return Err(crate::engine::EditorError::TransactionUnsupported {
5962 reason: "name mutations must use Engine's prepared logged-name APIs".to_string(),
5963 });
5964 }
5965
5966 self.clear_logged_cell_format_states(&new_events);
5969 for ev in &new_events {
5970 self.mirror_forward_change_to_arrow(ev);
5971 }
5972 for ev in &new_events {
5973 self.record_change_for_event(ev);
5974 }
5975
5976 if self.action_depth == 0 {
5979 let mut impact =
5980 Self::classify_change_events(&new_events, LoggedEditDirection::Original);
5981 if new_runs {
5982 impact = impact.max(LoggedEditImpact::Topology);
5983 }
5984 self.apply_logged_edit_impact(impact, invalidation_baseline);
5985 }
5986
5987 Ok(ret)
5988 }
5989
5990 pub(crate) fn preflight_replay_admission(
5991 &mut self,
5992 events: &[ChangeEvent],
5993 forward: bool,
5994 ) -> Result<(), crate::engine::EditorError> {
5995 if !self.graph_admission_enabled() {
5996 return Ok(());
5997 }
5998 let mut vertex_delta = 0i128;
5999 let mut edge_delta = 0i128;
6000 let mut added_vertices = 0usize;
6001 let mut added_edges = 0usize;
6002 let mut formula_cells = BTreeSet::new();
6003 for event in events {
6004 match event {
6005 ChangeEvent::AddVertex {
6006 formula,
6007 coord,
6008 sheet_id,
6009 ..
6010 } => {
6011 let delta = if forward { 1 } else { -1 };
6012 vertex_delta += delta;
6013 if forward {
6014 added_vertices = added_vertices.saturating_add(1);
6015 if formula.is_some() {
6016 formula_cells.insert((*sheet_id, coord.row(), coord.col()));
6017 }
6018 }
6019 }
6020 ChangeEvent::RemoveVertex {
6021 old_formula,
6022 coord,
6023 sheet_id,
6024 ..
6025 } => {
6026 let delta = if forward { -1 } else { 1 };
6027 vertex_delta += delta;
6028 if !forward {
6029 added_vertices = added_vertices.saturating_add(1);
6030 if old_formula.is_some()
6031 && let (Some(sheet_id), Some(coord)) = (sheet_id, coord)
6032 {
6033 formula_cells.insert((*sheet_id, coord.row(), coord.col()));
6034 }
6035 }
6036 }
6037 ChangeEvent::EdgeAdded { .. } => {
6038 let delta = if forward { 1 } else { -1 };
6039 edge_delta += delta;
6040 if forward {
6041 added_edges = added_edges.saturating_add(1);
6042 }
6043 }
6044 ChangeEvent::EdgeRemoved { .. } => {
6045 let delta = if forward { -1 } else { 1 };
6046 edge_delta += delta;
6047 if !forward {
6048 added_edges = added_edges.saturating_add(1);
6049 }
6050 }
6051 ChangeEvent::SetFormula {
6052 addr, old_formula, ..
6053 } if (forward || old_formula.is_some()) => {
6054 formula_cells.insert((addr.sheet_id, addr.coord.row(), addr.coord.col()));
6055 }
6056 _ => {}
6057 }
6058 }
6059 let stats = self.graph.baseline_stats();
6060 let final_vertices = i128::try_from(stats.graph_vertex_count)
6061 .ok()
6062 .and_then(|count| count.checked_add(vertex_delta))
6063 .and_then(|count| usize::try_from(count).ok())
6064 .ok_or_else(|| {
6065 crate::engine::EditorError::Excel(
6066 ExcelError::new(ExcelErrorKind::NImpl)
6067 .with_message("replay vertex count overflow"),
6068 )
6069 })?;
6070 let final_edges = i128::try_from(stats.graph_edge_count)
6071 .ok()
6072 .and_then(|count| count.checked_add(edge_delta))
6073 .and_then(|count| usize::try_from(count).ok())
6074 .ok_or_else(|| {
6075 crate::engine::EditorError::Excel(
6076 ExcelError::new(ExcelErrorKind::NImpl)
6077 .with_message("replay edge count overflow"),
6078 )
6079 })?;
6080 self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
6081 final_vertices,
6082 final_edges,
6083 materialization_cells: formula_cells.len() as u64,
6084 added_vertices,
6085 added_edges,
6086 })
6087 .map_err(crate::engine::EditorError::Excel)
6088 }
6089
6090 pub fn undo_logged(
6095 &mut self,
6096 undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6097 log: &mut crate::engine::ChangeLog,
6098 ) -> Result<(), crate::engine::EditorError> {
6099 self.ensure_not_poisoned()?;
6100 let pending_events = log
6101 .last_group_indices()
6102 .into_iter()
6103 .map(|index| log.events()[index].clone())
6104 .collect::<Vec<_>>();
6105 self.preflight_replay_admission(&pending_events, false)?;
6106 let invalidation_baseline = self.invalidation_baseline();
6107 self.invalidate_for_change_events(
6110 &pending_events,
6111 LoggedEditDirection::InverseReplay,
6112 invalidation_baseline,
6113 );
6114 let batch = undo.undo(&mut self.graph, log)?;
6115 for item in batch.iter().rev() {
6116 self.apply_inverse_row_visibility_event(&item.event);
6117 self.apply_inverse_staged_formula_event(&item.event);
6118 }
6119 if !batch.is_empty() {
6120 let events = batch
6121 .iter()
6122 .map(|item| item.event.clone())
6123 .collect::<Vec<_>>();
6124 self.clear_logged_cell_format_states(&events);
6125 }
6126 self.mirror_undo_batch_to_arrow(&batch);
6127 if !batch.is_empty() {
6128 for item in &batch {
6129 self.record_inverse_change_for_event(&item.event);
6130 }
6131 }
6132 crate::engine::trace::fz_event!(
6133 tracing::Level::INFO,
6134 "history",
6135 "history.replay",
6136 op = "undo",
6137 events_replayed = batch.len(),
6138 ownership_resyncs = batch.len()
6139 );
6140 Ok(())
6141 }
6142
6143 pub fn redo_logged(
6144 &mut self,
6145 undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6146 log: &mut crate::engine::ChangeLog,
6147 ) -> Result<(), crate::engine::EditorError> {
6148 self.ensure_not_poisoned()?;
6149 let pending_events = undo.pending_redo_events();
6150 self.preflight_replay_admission(&pending_events, true)?;
6151 let invalidation_baseline = self.invalidation_baseline();
6152 self.invalidate_for_change_events(
6153 &pending_events,
6154 LoggedEditDirection::ForwardReplay,
6155 invalidation_baseline,
6156 );
6157 let batch = undo.redo(&mut self.graph, log)?;
6158 for item in &batch {
6159 self.apply_forward_row_visibility_event(&item.event);
6160 self.apply_forward_staged_formula_event(&item.event);
6161 }
6162 if !batch.is_empty() {
6163 let events = batch
6164 .iter()
6165 .map(|item| item.event.clone())
6166 .collect::<Vec<_>>();
6167 self.clear_logged_cell_format_states(&events);
6168 }
6169 self.mirror_redo_batch_to_arrow(&batch);
6170 if !batch.is_empty() {
6171 for item in &batch {
6172 self.record_change_for_event(&item.event);
6173 }
6174 }
6175 crate::engine::trace::fz_event!(
6176 tracing::Level::INFO,
6177 "history",
6178 "history.replay",
6179 op = "redo",
6180 events_replayed = batch.len(),
6181 ownership_resyncs = batch.len()
6182 );
6183 Ok(())
6184 }
6185
6186 pub fn undo_action(
6190 &mut self,
6191 undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6192 ) -> Result<(), crate::engine::EditorError> {
6193 self.ensure_not_poisoned()?;
6194 let Some(journal) = undo.pop_undo_action() else {
6195 return Ok(());
6196 };
6197 if let Err(error) = self.preflight_replay_admission(&journal.graph.events, false) {
6198 undo.push_done_action(journal);
6199 return Err(error);
6200 }
6201 let invalidation_baseline = self.invalidation_baseline();
6202
6203 self.invalidate_for_action_journal(
6204 &journal,
6205 LoggedEditDirection::InverseReplay,
6206 invalidation_baseline,
6207 );
6208 journal.graph.undo(&mut self.graph)?;
6209 self.apply_inverse_row_visibility_events(&journal.graph.events);
6210 self.apply_arrow_undo_batch(&journal.arrow, true);
6211 if !journal.graph.is_empty() || !journal.arrow.is_empty() {
6212 for event in &journal.graph.events {
6213 self.record_inverse_change_for_event(event);
6214 }
6215 }
6216
6217 #[cfg(feature = "tracing")]
6218 let events_replayed = journal.graph.events.len();
6219 crate::engine::trace::fz_event!(
6220 tracing::Level::INFO,
6221 "history",
6222 "history.replay",
6223 op = "undo",
6224 events_replayed,
6225 ownership_resyncs = events_replayed
6226 );
6227 undo.push_redo_action(journal);
6228 Ok(())
6229 }
6230
6231 pub fn redo_action(
6235 &mut self,
6236 undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6237 ) -> Result<(), crate::engine::EditorError> {
6238 self.ensure_not_poisoned()?;
6239 let Some(journal) = undo.pop_redo_action() else {
6240 return Ok(());
6241 };
6242 if let Err(error) = self.preflight_replay_admission(&journal.graph.events, true) {
6243 undo.push_redo_action(journal);
6244 return Err(error);
6245 }
6246 let invalidation_baseline = self.invalidation_baseline();
6247 self.invalidate_for_action_journal(
6248 &journal,
6249 LoggedEditDirection::ForwardReplay,
6250 invalidation_baseline,
6251 );
6252 journal.graph.redo(&mut self.graph)?;
6253 self.apply_forward_row_visibility_events(&journal.graph.events);
6254 self.apply_arrow_undo_batch(&journal.arrow, false);
6255 if !journal.graph.is_empty() || !journal.arrow.is_empty() {
6256 for event in &journal.graph.events {
6257 self.record_change_for_event(event);
6258 }
6259 }
6260
6261 #[cfg(feature = "tracing")]
6262 let events_replayed = journal.graph.events.len();
6263 crate::engine::trace::fz_event!(
6264 tracing::Level::INFO,
6265 "history",
6266 "history.replay",
6267 op = "redo",
6268 events_replayed,
6269 ownership_resyncs = events_replayed
6270 );
6271 undo.push_done_action(journal);
6272 Ok(())
6273 }
6274
6275 fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
6276 let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
6277 let row = addr.coord.row() + 1;
6279 let col = addr.coord.col() + 1;
6280 (sheet, row, col)
6281 }
6282
6283 fn mirror_undo_batch_to_arrow(
6284 &mut self,
6285 batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
6286 ) {
6287 for item in batch.iter().rev() {
6289 self.mirror_inverse_change_to_arrow(&item.event);
6290 }
6291 }
6292
6293 fn mirror_redo_batch_to_arrow(
6294 &mut self,
6295 batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
6296 ) {
6297 for item in batch.iter() {
6299 self.mirror_forward_change_to_arrow(&item.event);
6300 }
6301 }
6302
6303 fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
6304 use crate::engine::ChangeEvent;
6305 use formualizer_common::LiteralValue;
6306
6307 match ev {
6308 ChangeEvent::SetValue {
6309 addr,
6310 old_value,
6311 old_formula,
6312 ..
6313 } => {
6314 let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6315 if old_formula.is_some() {
6316 self.clear_delta_overlay_cell(&sheet, row, col);
6317 } else {
6318 let v = old_value.clone().unwrap_or(LiteralValue::Empty);
6319 self.mirror_value_to_overlay(&sheet, row, col, &v);
6320 }
6321 }
6322 ChangeEvent::SetFormula {
6323 addr,
6324 old_value,
6325 old_formula,
6326 ..
6327 } => {
6328 let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6329 if old_formula.is_some() {
6330 self.clear_delta_overlay_cell(&sheet, row, col);
6331 } else {
6332 let v = old_value.clone().unwrap_or(LiteralValue::Empty);
6333 self.mirror_value_to_overlay(&sheet, row, col, &v);
6334 }
6335 }
6336 ChangeEvent::SpillCommitted { old, new, .. } => {
6337 self.mirror_spill_snapshot(new, true);
6339 if let Some(snap) = old {
6340 self.mirror_spill_snapshot(snap, false);
6341 }
6342 }
6343 ChangeEvent::SpillCleared { old, .. } => {
6344 self.mirror_spill_snapshot(old, false);
6346 }
6347 ChangeEvent::SetRowVisibility { .. } => {
6348 }
6350 _ => {}
6351 }
6352 }
6353
6354 fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
6355 use crate::engine::ChangeEvent;
6356
6357 match ev {
6358 ChangeEvent::SetValue { addr, new, .. } => {
6359 let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6360 self.mirror_value_to_overlay(&sheet, row, col, new);
6361 }
6362 ChangeEvent::SetFormula { addr, .. } => {
6363 let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6364 self.clear_delta_overlay_cell(&sheet, row, col);
6365 }
6367 ChangeEvent::SpillCommitted { old, new, .. } => {
6368 if let Some(snap) = old {
6369 self.mirror_spill_snapshot(snap, true);
6370 }
6371 self.mirror_spill_snapshot(new, false);
6372 }
6373 ChangeEvent::SpillCleared { old, .. } => {
6374 self.mirror_spill_snapshot(old, true);
6375 }
6376 ChangeEvent::SetRowVisibility { .. } => {
6377 }
6379 _ => {
6380 }
6382 }
6383 }
6384
6385 fn mirror_spill_snapshot(
6386 &mut self,
6387 snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
6388 clear_only: bool,
6389 ) {
6390 use formualizer_common::LiteralValue;
6391
6392 let Some(first) = snap.target_cells.first() else {
6393 return;
6394 };
6395 for cell in &snap.target_cells {
6396 let r_off = cell.coord.row().saturating_sub(first.coord.row()) as usize;
6397 let c_off = cell.coord.col().saturating_sub(first.coord.col()) as usize;
6398 let Some(v) = snap.values.get(r_off).and_then(|row| row.get(c_off)) else {
6399 continue;
6400 };
6401 let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
6402 if clear_only && cell != first && self.read_cell_formula_ast(&sheet, r, c).is_some() {
6406 continue;
6407 }
6408 let out = if clear_only {
6409 LiteralValue::Empty
6410 } else {
6411 v.clone()
6412 };
6413 self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
6414 }
6415 }
6416
6417 pub fn set_default_sheet_by_name(&mut self, name: &str) {
6421 self.assert_not_poisoned();
6422 self.graph.set_default_sheet_by_name(name);
6423 }
6424
6425 pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
6429 self.assert_not_poisoned();
6430 self.graph.set_default_sheet_by_id(id);
6431 }
6432
6433 pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
6437 self.assert_not_poisoned();
6438 self.graph.set_sheet_index_mode(mode);
6439 }
6440
6441 #[cfg(feature = "test-support")]
6442 #[doc(hidden)]
6443 pub fn mark_all_formulas_dirty_for_test(&mut self) {
6447 self.assert_not_poisoned();
6448 self.mark_all_formula_vertices_dirty();
6449 }
6450
6451 #[cfg(feature = "test-support")]
6452 #[doc(hidden)]
6453 pub fn take_criteria_mask_work_for_test() -> (usize, usize) {
6454 criteria_mask_test_hooks::take_mask_work()
6455 }
6456
6457 #[cfg(feature = "test-support")]
6458 #[doc(hidden)]
6459 pub fn lookup_index_cache_report_for_test(&self) -> LookupIndexCacheReport {
6460 self.lookup_index_cache.report()
6461 }
6462
6463 #[cfg(any(test, feature = "benchmark_internal"))]
6464 #[doc(hidden)]
6465 pub fn reset_recalc_reuse_probe(&mut self) {
6469 self.assert_not_poisoned();
6470 *self.recalc_reuse_probe.get_mut().unwrap() = RecalcReuseProbe::default();
6471 }
6472
6473 #[cfg(any(test, feature = "benchmark_internal"))]
6474 #[doc(hidden)]
6475 pub fn recalc_reuse_probe(&self) -> RecalcReuseProbe {
6476 let mut probe = self.recalc_reuse_probe.lock().unwrap().clone();
6477 if let Some(cached) = self.cached_static_schedule.as_ref() {
6478 let entry_bytes = |e: &CachedScheduleEntry| {
6479 std::mem::size_of::<CachedScheduleEntry>()
6480 + e.candidate_vertices.heap_bytes()
6481 + std::mem::size_of::<crate::engine::Schedule>()
6482 + 2 * std::mem::size_of::<usize>()
6483 + schedule_probe_retained_bytes(&e.schedule)
6484 };
6485 probe.schedule_retained_bytes = entry_bytes(cached)
6486 + self.recent_schedules.iter().map(entry_bytes).sum::<usize>()
6487 + self.base_schedule.as_ref().map_or(0, entry_bytes)
6488 + self.recent_schedules.capacity() * std::mem::size_of::<CachedScheduleEntry>();
6489 }
6490 probe
6491 }
6492
6493 #[cfg(test)]
6494 pub(crate) fn cached_static_schedule_for_test(&self) -> Option<Arc<crate::engine::Schedule>> {
6495 self.cached_static_schedule
6496 .as_ref()
6497 .map(|cached| Arc::clone(&cached.schedule))
6498 }
6499
6500 fn clear_cached_static_schedule(&mut self) {
6501 self.cached_static_schedule = None;
6502 self.recent_schedules.clear();
6503 self.base_schedule = None;
6504 }
6505
6506 fn retain_recent_schedule(&mut self, entry: CachedScheduleEntry) {
6509 const RECENT_SCHEDULES: usize = 8;
6510 const RECENT_SCHEDULE_VERTICES: usize = 65_536;
6511 let revision = self.schedule_cache_authority_revision();
6512 let epoch = self.topology_epoch;
6513 self.recent_schedules
6514 .retain(|e| e.topology_epoch == epoch && e.authority_revision == revision);
6515 let current =
6516 |e: &CachedScheduleEntry| e.topology_epoch == epoch && e.authority_revision == revision;
6517 if self.base_schedule.as_ref().is_some_and(|b| !current(b)) {
6518 self.base_schedule = None;
6519 }
6520 let mut entry = entry;
6523 if current(&entry)
6524 && entry.candidate_vertices.len() > BASE_SCHEDULE_MIN_VERTICES
6525 && self
6526 .base_schedule
6527 .as_ref()
6528 .is_none_or(|b| entry.candidate_vertices.len() > b.candidate_vertices.len())
6529 {
6530 match self.base_schedule.replace(entry) {
6531 Some(previous) => entry = previous,
6532 None => return,
6533 }
6534 }
6535 if entry.topology_epoch != epoch
6536 || entry.authority_revision != revision
6537 || entry.candidate_vertices.len() > RECENT_SCHEDULE_VERTICES / 4
6538 {
6539 return;
6540 }
6541 self.recent_schedules.push(entry);
6542 let mut total: usize = self
6543 .recent_schedules
6544 .iter()
6545 .map(|e| e.candidate_vertices.len())
6546 .sum();
6547 while self.recent_schedules.len() > RECENT_SCHEDULES || total > RECENT_SCHEDULE_VERTICES {
6548 let oldest = self.recent_schedules.remove(0);
6549 total -= oldest.candidate_vertices.len();
6550 }
6551 }
6552
6553 fn invalidation_baseline(&self) -> InvalidationBaseline {
6554 InvalidationBaseline {
6555 snapshot_id: self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed),
6556 topology_epoch: self.topology_epoch,
6557 }
6558 }
6559
6560 fn classify_change_events(
6561 events: &[crate::engine::ChangeEvent],
6562 direction: LoggedEditDirection,
6563 ) -> LoggedEditImpact {
6564 use crate::engine::ChangeEvent;
6565
6566 events
6567 .iter()
6568 .map(|event| match event {
6569 ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
6570 LoggedEditImpact::NoOp
6571 }
6572 ChangeEvent::SetRowVisibility { .. }
6573 | ChangeEvent::SetValue {
6574 old_formula: None,
6575 old_value: Some(_),
6576 ..
6577 } => LoggedEditImpact::DataOnly,
6578 ChangeEvent::SetValue {
6582 old_formula: None,
6583 old_value: None,
6584 ..
6585 } if direction == LoggedEditDirection::Original => LoggedEditImpact::DataOnly,
6586 ChangeEvent::SetValue { .. }
6587 | ChangeEvent::SetFormula { .. }
6588 | ChangeEvent::AddVertex { .. }
6589 | ChangeEvent::RemoveVertex { .. }
6590 | ChangeEvent::VertexMoved { .. }
6591 | ChangeEvent::FormulaAdjusted { .. }
6592 | ChangeEvent::NamedRangeAdjusted { .. }
6593 | ChangeEvent::TableResized { .. }
6594 | ChangeEvent::EdgeAdded { .. }
6595 | ChangeEvent::EdgeRemoved { .. }
6596 | ChangeEvent::DefineName { .. }
6597 | ChangeEvent::UpdateName { .. }
6598 | ChangeEvent::DeleteName { .. }
6599 | ChangeEvent::SpillCommitted { .. }
6600 | ChangeEvent::SpillCleared { .. }
6601 | ChangeEvent::StagedFormulaCellChanged { .. } => LoggedEditImpact::Topology,
6602 })
6603 .max()
6604 .unwrap_or(LoggedEditImpact::NoOp)
6605 }
6606
6607 fn classify_arrow_undo(arrow: &crate::engine::ArrowUndoBatch) -> LoggedEditImpact {
6608 use crate::engine::ArrowOp;
6609
6610 arrow
6611 .ops
6612 .iter()
6613 .map(|op| match op {
6614 ArrowOp::SetDeltaCell { .. } | ArrowOp::SetComputedCell { .. } => {
6615 LoggedEditImpact::DataOnly
6616 }
6617 ArrowOp::RestoreComputedRect { .. }
6618 | ArrowOp::InsertRows { .. }
6619 | ArrowOp::InsertCols { .. } => LoggedEditImpact::Topology,
6620 })
6621 .max()
6622 .unwrap_or(LoggedEditImpact::NoOp)
6623 }
6624
6625 fn apply_logged_edit_impact(
6626 &mut self,
6627 impact: LoggedEditImpact,
6628 baseline: InvalidationBaseline,
6629 ) {
6630 match impact {
6631 LoggedEditImpact::NoOp => {}
6632 LoggedEditImpact::DataOnly => {
6633 if self.topology_epoch == baseline.topology_epoch
6634 && self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
6635 == baseline.snapshot_id
6636 {
6637 self.mark_data_edited();
6638 }
6639 }
6640 LoggedEditImpact::Topology => {
6641 if self.topology_epoch == baseline.topology_epoch {
6644 self.mark_topology_edited();
6645 }
6646 }
6647 }
6648 }
6649
6650 fn invalidate_for_change_events(
6651 &mut self,
6652 events: &[crate::engine::ChangeEvent],
6653 direction: LoggedEditDirection,
6654 baseline: InvalidationBaseline,
6655 ) {
6656 self.apply_logged_edit_impact(Self::classify_change_events(events, direction), baseline);
6657 }
6658
6659 fn invalidate_for_action_journal(
6660 &mut self,
6661 journal: &crate::engine::ActionJournal,
6662 direction: LoggedEditDirection,
6663 baseline: InvalidationBaseline,
6664 ) {
6665 let impact = Self::classify_change_events(&journal.graph.events, direction)
6666 .max(Self::classify_arrow_undo(&journal.arrow));
6667 self.apply_logged_edit_impact(impact, baseline);
6668 }
6669
6670 pub fn mark_data_edited(&mut self) {
6676 self.assert_not_poisoned();
6677 self.lookup_index_cache.clear();
6678 self.snapshot_id
6679 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6680 self.has_edited = true;
6681 }
6682
6683 pub fn mark_topology_edited(&mut self) {
6688 self.assert_not_poisoned();
6689 self.arrow_sheet_index.take();
6690 self.lookup_index_cache.clear();
6691 self.snapshot_id
6692 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6693 self.topology_epoch = self.topology_epoch.wrapping_add(1);
6694 self.graph.bump_topology_revision();
6695 self.clear_cached_static_schedule();
6696 if let Some(ledger) = self.active_resource_ledger.as_mut() {
6697 let released = ledger.account_mixed_cache(0);
6698 debug_assert!(released.is_ok());
6699 }
6700 self.has_edited = true;
6701 self.graph.authority_sync_eager();
6705 }
6706
6707 fn mark_all_formula_vertices_dirty(&mut self) {
6708 let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
6709 for vertex in vertices {
6710 self.graph.mark_vertex_dirty(vertex);
6711 }
6712 }
6713
6714 fn mark_moved_formula_vertices_dirty(
6715 &mut self,
6716 summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
6717 ) {
6718 for vertex in &summary.vertices_moved {
6719 if self.graph.has_formula(*vertex) {
6720 self.graph.mark_vertex_dirty(*vertex);
6721 }
6722 }
6723 }
6724
6725 pub fn sheet_store(&self) -> &SheetStore {
6727 &self.arrow_sheets
6728 }
6729
6730 pub(crate) fn has_row_visibility_state(&self) -> bool {
6733 !self.row_visibility.is_empty()
6734 }
6735
6736 pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
6741 self.assert_not_poisoned();
6742 self.arrow_sheet_index.take();
6743 &mut self.arrow_sheets
6744 }
6745
6746 pub fn has_staged_formulas(&self) -> bool {
6747 !self.staged_formulas.is_empty()
6748 }
6749
6750 pub fn staged_formula_count(&self) -> usize {
6751 self.staged_formulas.values().map(StagedSheet::len).sum()
6752 }
6753
6754 pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
6759 self.assert_not_poisoned();
6760 self.staged_formulas
6761 .entry(sheet.to_string())
6762 .or_default()
6763 .stage(row, col, text);
6764 self.staged_formula_index.stage(sheet, row, col);
6765 if let Some(sheet) = self.graph.sheet_id(sheet) {
6766 self.invalidate_pending_spills(StructuralScope::Cell {
6767 sheet,
6768 row: row.saturating_sub(1),
6769 col: col.saturating_sub(1),
6770 });
6771 }
6772 }
6773
6774 fn index_removed_staged_sheet(&mut self, sheet: &str, staged: &StagedSheet) {
6775 for (row, col, _) in &staged.entries {
6776 self.staged_formula_index.remove(sheet, *row, *col);
6777 }
6778 if staged.deferred_package.is_some() {
6779 self.staged_formula_index.set_package(sheet, None);
6780 }
6781 }
6782
6783 fn restore_staged_sheet(&mut self, sheet: String, staged: StagedSheet) {
6784 self.staged_formulas.insert(sheet, staged);
6785 }
6786
6787 #[doc(hidden)]
6788 pub fn source_formula_ingress(&mut self) -> SourceFormulaIngress<'_, R> {
6792 self.assert_not_poisoned();
6793 SourceFormulaIngress { engine: self }
6794 }
6795
6796 #[doc(hidden)]
6797 #[doc(hidden)]
6800 #[cfg(any(test, feature = "test-support"))]
6801 pub fn set_before_prepared_span_commit_hook(
6805 &mut self,
6806 hook: impl FnOnce() + Send + Sync + 'static,
6807 ) {
6808 self.assert_not_poisoned();
6809 self.before_prepared_span_commit_hook = Some(Box::new(hook));
6810 }
6811
6812 #[doc(hidden)]
6813 #[cfg(test)]
6815 pub(crate) fn set_before_target_preparation_commit_hook(
6816 &mut self,
6817 hook: impl FnOnce() + Send + Sync + 'static,
6818 ) {
6819 self.before_target_preparation_commit_hook = Some(Box::new(hook));
6820 }
6821
6822 fn stage_deferred_formula_package(&mut self, package: crate::engine::DeferredFormulaPackage) {
6823 if let Ok(replay) = package.replay.lock()
6824 && let Some(footprint) = replay.selection_cache_footprint()
6825 && !self
6826 .source_cache_footprints
6827 .iter()
6828 .any(|known| known.ptr_eq(&footprint))
6829 {
6830 self.source_cache_footprints.push(footprint);
6831 }
6832 let sheet = package.sheet_name.clone();
6833 let staged = self.staged_formulas.entry(sheet.clone()).or_default();
6834 debug_assert!(staged.deferred_package.is_none());
6835 staged.deferred_package = Some(package);
6836 staged.reconcile_attached_deferred_package();
6837 self.staged_formula_index
6838 .set_package(&sheet, staged.deferred_package.as_ref());
6839 if let Some(sheet_id) = self.graph.sheet_id(&sheet) {
6840 let package = self
6841 .staged_formulas
6842 .get(&sheet)
6843 .and_then(|staged| staged.deferred_package.as_ref());
6844 for &(vertex, anchor, region) in &self.blocked_pending_spills {
6845 if anchor.sheet_id == sheet_id
6846 && self.graph.vertex_exists(vertex)
6847 && self.graph.get_cell_ref(vertex) == Some(anchor)
6848 && self.staged_formula_index.package_occupies_spill(
6849 &sheet,
6850 (anchor.coord.row() + 1, anchor.coord.col() + 1),
6851 (
6852 region.rows.query_bounds().1 + 1,
6853 region.cols.query_bounds().1 + 1,
6854 ),
6855 |point| package.is_some_and(|package| package.suppressed.contains(&point)),
6856 )
6857 {
6858 self.graph.mark_vertex_dirty(vertex);
6859 }
6860 }
6861 }
6862 }
6863
6864 pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
6868 self.assert_not_poisoned();
6869 let mut removed = None;
6870 let mut remove_sheet = false;
6871 let mut had_package = false;
6872 if let Some(entries) = self.staged_formulas.get_mut(sheet) {
6873 had_package = entries.deferred_package.is_some();
6874 removed = entries.remove(row, col);
6875 remove_sheet = entries.is_empty();
6876 }
6877 let ordinary_removed = self.staged_formula_index.remove(sheet, row, col);
6878 if !ordinary_removed && had_package {
6879 self.staged_formula_index.touch_package(sheet);
6880 }
6881 if remove_sheet {
6882 self.staged_formulas.remove(sheet);
6883 self.staged_formula_index.set_package(sheet, None);
6884 }
6885 if (ordinary_removed || had_package)
6886 && let Some(sheet) = self.graph.sheet_id(sheet)
6887 {
6888 self.invalidate_pending_spills(StructuralScope::Cell {
6889 sheet,
6890 row: row.saturating_sub(1),
6891 col: col.saturating_sub(1),
6892 });
6893 }
6894 removed
6895 }
6896
6897 pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
6901 self.assert_not_poisoned();
6902 if self.staged_formulas.remove(sheet).is_some() {
6903 self.staged_formula_index.clear_sheet(sheet);
6904 if let Some(sheet_id) = self.graph.sheet_id(sheet) {
6905 self.invalidate_pending_spills(StructuralScope::Sheet(sheet_id));
6906 }
6907 }
6908 }
6909
6910 pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
6914 self.assert_not_poisoned();
6915 let Some(entries) = self.staged_formulas.remove(old) else {
6916 return;
6917 };
6918 self.staged_formula_index.clear_sheet(old);
6919 let (formulas, mut package) = entries.into_parts();
6920 for (row, col, text) in formulas {
6921 self.stage_formula_text(new, row, col, text);
6922 }
6923 if let Some(package) = package.as_mut() {
6924 package.sheet_name = new.to_string();
6925 }
6926 if let Some(package) = package {
6927 self.stage_deferred_formula_package(package);
6928 }
6929 }
6930
6931 pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
6933 self.staged_formulas
6934 .get(sheet)
6935 .and_then(|v| v.get(row, col))
6936 }
6937
6938 pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
6939 &self.formula_parse_diagnostics
6940 }
6941
6942 pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
6946 self.assert_not_poisoned();
6947 std::mem::take(&mut self.formula_parse_diagnostics)
6948 }
6949
6950 pub fn clear_formula_parse_diagnostics(&mut self) {
6954 self.assert_not_poisoned();
6955 self.formula_parse_diagnostics.clear();
6956 }
6957
6958 pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
6959 self.last_formula_ingest_report.as_ref()
6960 }
6961
6962 pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
6963 &self.formula_ingest_report_total
6964 }
6965
6966 #[cfg(test)]
6967 pub(crate) fn set_before_target_planning_snapshot_hook_for_test(
6968 &mut self,
6969 hook: impl FnOnce() + Send + Sync + 'static,
6970 ) {
6971 self.before_target_planning_snapshot_hook = Some(Box::new(hook));
6972 }
6973
6974 #[cfg(test)]
6975 pub(crate) fn inject_target_semantic_stale_once_for_test(&mut self) {
6976 self.inject_target_semantic_stale_once_for_test = true;
6977 }
6978
6979 #[cfg(test)]
6980 pub(crate) fn force_virtual_dep_changes_for_test(&mut self, rounds: usize) {
6981 self.force_virtual_dep_changes_remaining_for_test = rounds;
6982 }
6983
6984 #[cfg(test)]
6985 pub(crate) fn fail_evaluation_commit_preflight_once_for_test(&mut self) {
6986 self.fail_evaluation_commit_preflight_once_for_test = true;
6987 }
6988
6989 #[cfg(test)]
6990 pub(crate) fn set_target_preparation_fault_for_test(
6991 &mut self,
6992 fault: crate::engine::target_preparation::TargetPreparationFault,
6993 ) {
6994 self.target_preparation_fault_for_test = Some(fault);
6995 }
6996
6997 #[cfg(test)]
6998 pub(crate) fn staged_formula_index_revision_for_test(&self) -> u64 {
6999 self.staged_formula_index.revision()
7000 }
7001
7002 #[cfg(test)]
7003 pub(crate) fn deferred_package_for_test(
7004 &self,
7005 sheet: &str,
7006 ) -> &crate::engine::DeferredFormulaPackage {
7007 self.staged_formulas
7008 .get(sheet)
7009 .unwrap()
7010 .deferred_package
7011 .as_ref()
7012 .unwrap()
7013 }
7014
7015 #[cfg(test)]
7016 pub(crate) fn staged_formula_index_is_consistent_for_test(&self) -> bool {
7017 let ordinary_storage = self
7018 .staged_formulas
7019 .values()
7020 .map(|sheet| sheet.entries.len())
7021 .sum::<usize>();
7022 let package_storage = self
7023 .staged_formulas
7024 .values()
7025 .filter(|sheet| sheet.deferred_package.is_some())
7026 .count();
7027 ordinary_storage == self.staged_formula_index.ordinary_count()
7028 && package_storage == self.staged_formula_index.package_count()
7029 && self.staged_formulas.iter().all(|(name, sheet)| {
7030 sheet.entries.iter().all(|(row, col, _)| {
7031 let leases = self
7032 .staged_formula_index
7033 .leases_in_region(name, *row, *col, *row, *col);
7034 leases.len() == 1 && leases[0].row == *row && leases[0].col == *col
7035 })
7036 })
7037 }
7038
7039 #[cfg(test)]
7040 pub(crate) fn evaluation_request_begin_count_for_test(&self) -> u64 {
7041 self.evaluation_request_begin_count_for_test
7042 }
7043
7044 #[cfg(test)]
7045 pub(crate) fn set_before_legacy_fallback_final_provider_sample_hook(
7046 &mut self,
7047 hook: impl FnOnce() + Send + Sync + 'static,
7048 ) {
7049 self.before_legacy_fallback_final_provider_sample_hook = Some(Box::new(hook));
7050 }
7051
7052 #[cfg(test)]
7053 pub(crate) fn set_after_eager_proposal_commit_hook(
7054 &mut self,
7055 hook: impl FnOnce() + Send + Sync + 'static,
7056 ) {
7057 self.after_eager_proposal_commit_hook = Some(Box::new(hook));
7058 }
7059
7060 #[cfg(test)]
7061 pub(crate) fn topology_epoch_for_test(&self) -> u64 {
7062 self.topology_epoch
7063 }
7064
7065 #[cfg(test)]
7066 pub(crate) fn graph_topology_revision_for_test(&self) -> u64 {
7067 self.graph.topology_revision()
7068 }
7069
7070 fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
7071 self.formula_ingest_report_total.mode = report.mode;
7072 self.formula_ingest_report_total.accumulate(&report);
7073 self.last_formula_ingest_report = Some(report);
7074 }
7075
7076 fn collect_planning_function_requests(
7077 ast: &ASTNode,
7078 requests: &mut Vec<(String, String, usize)>,
7079 ) {
7080 match &ast.node_type {
7081 ASTNodeType::Function { name, args } => {
7082 requests.push((String::new(), name.clone(), args.len()));
7083 for arg in args {
7084 Self::collect_planning_function_requests(arg, requests);
7085 }
7086 }
7087 ASTNodeType::BinaryOp { left, right, .. } => {
7088 Self::collect_planning_function_requests(left, requests);
7089 Self::collect_planning_function_requests(right, requests);
7090 }
7091 ASTNodeType::UnaryOp { expr, .. } => {
7092 Self::collect_planning_function_requests(expr, requests);
7093 }
7094 ASTNodeType::Call { callee, args } => {
7095 Self::collect_planning_function_requests(callee, requests);
7096 for arg in args {
7097 Self::collect_planning_function_requests(arg, requests);
7098 }
7099 }
7100 ASTNodeType::Array(rows) => {
7101 for cell in rows.iter().flatten() {
7102 Self::collect_planning_function_requests(cell, requests);
7103 }
7104 }
7105 ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {}
7106 }
7107 }
7108
7109 fn prepared_function_semantics_changed(
7110 &self,
7111 preparation: &crate::engine::FormulaCompressedPreparation,
7112 guard: &crate::function_registry::SemanticEpochReadGuard,
7113 ) -> bool {
7114 if preparation.function_semantic_epoch == guard.epoch() {
7115 return false;
7116 }
7117
7118 guard
7119 .semantic_changes_affect_requests_since(preparation.function_semantic_epoch, Vec::new())
7120 }
7121
7122 pub(crate) fn prepare_source_formula_families(
7123 &mut self,
7124 sheet_name: &str,
7125 families: &[crate::engine::SourceFormulaFamily],
7126 ) -> crate::engine::FormulaCompressedPreparation {
7127 crate::engine::FormulaCompressedPreparation {
7128 engine_token: Arc::clone(&self.source_formula_token),
7129 function_semantic_epoch: crate::function_registry::semantic_epoch(),
7130 function_provider_revision: None,
7131 function_semantics_used: false,
7132 sheet_name: Arc::from(sheet_name),
7133 rejected: BTreeMap::new(),
7134 eager_replay: Vec::new(),
7135 preparation_spool_replays: 0,
7136 clean_rejected_anchor_counts: [0; 3],
7137 fragmented_rejected_anchor_counts: [0; 3],
7138 exact_replay: None,
7139 replay_disposition: crate::engine::FormulaReplayDisposition::default(),
7140 }
7141 }
7142
7143 fn prepare_source_formula_proposals(
7144 &mut self,
7145 sheet_name: &str,
7146 families: &[crate::engine::SourceFormulaFamily],
7147 authority_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
7148 replay_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
7149 formula_record_count: u64,
7150 replay: Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
7151 suppressed: &BTreeSet<(u32, u32)>,
7152 consumed: &FxHashSet<(crate::engine::SourceFamilyId, crate::engine::SourceCoord)>,
7153 consumed_engine: Option<&Arc<()>>,
7154 ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
7155 let mut preparation = self.prepare_source_formula_families(sheet_name, families);
7156 preparation.exact_replay = Some(Arc::clone(&replay));
7157 preparation
7158 .replay_disposition
7159 .register_consumed_members(consumed_engine, consumed.iter().copied());
7160 if !preparation
7161 .replay_disposition
7162 .consumed_engine_matches(&self.source_formula_token)
7163 {
7164 return Err(ExcelError::new(ExcelErrorKind::Value)
7165 .with_message("ResidualConsumedEngineMismatch"));
7166 }
7167 preparation
7168 .replay_disposition
7169 .extend_suppressed_excel_coords(suppressed.iter().copied());
7170 Ok(preparation)
7171 }
7172
7173 fn formula_batch_from_exact_replay(
7174 &mut self,
7175 sheet_name: &str,
7176 replayed: impl IntoIterator<Item = crate::engine::DeferredReplayFormula>,
7177 ) -> Result<FormulaIngestBatch, ExcelError> {
7178 let mut cache = rustc_hash::FxHashMap::default();
7179 let mut formulas = Vec::new();
7180 for record in replayed {
7181 let key = if record.text.starts_with('=') {
7182 record.text
7183 } else {
7184 format!("={}", record.text)
7185 };
7186 let ast_id = if let Some(cached) = cache.get(&key) {
7187 *cached
7188 } else {
7189 let parsed = match formualizer_parse::parser::parse(&key) {
7190 Ok(parsed) => parsed,
7191 Err(error) => {
7192 let Some(parsed) = self.handle_formula_parse_error(
7193 sheet_name,
7194 record.row,
7195 record.col,
7196 &key,
7197 error.to_string(),
7198 )?
7199 else {
7200 continue;
7201 };
7202 parsed
7203 }
7204 };
7205 let ast_id = self.intern_formula_ast(&parsed);
7206 cache.insert(key.clone(), ast_id);
7207 ast_id
7208 };
7209 formulas.push(
7210 FormulaIngestRecord::new(record.row, record.col, ast_id, Some(Arc::from(key)))
7211 .with_source_proof(record.source_order, record.family, record.partition_owner),
7212 );
7213 }
7214 Ok(FormulaIngestBatch::new(sheet_name.to_string(), formulas))
7215 }
7216
7217 fn prepare_target_combined_legacy_graph(
7218 &self,
7219 packages: &[PreparedTargetSourcePackage],
7220 ordinary: &[PreparedOrdinaryStagedFormula],
7221 ) -> Result<(PreparedLegacyGraphPlan, usize), ExcelError> {
7222 let mut planned_by_coord = BTreeMap::new();
7223 for package in packages {
7224 for (row, col, ast_id, plan) in &package.legacy {
7225 planned_by_coord.insert((package.sheet_id, *row, *col), (*ast_id, plan.clone()));
7226 }
7227 }
7228 for formula in ordinary {
7229 if let Some((ast_id, plan)) = formula.ast_id.zip(formula.plan.clone()) {
7230 planned_by_coord.insert(
7231 (formula.sheet_id, formula.lease.row, formula.lease.col),
7232 (ast_id, plan),
7233 );
7234 }
7235 }
7236 let planned = planned_by_coord
7237 .into_iter()
7238 .map(|((sheet_id, row, col), (ast_id, plan))| (sheet_id, row, col, ast_id, plan))
7239 .collect::<Vec<_>>();
7240 let formula_count = planned.len();
7241 let graph = self
7242 .graph
7243 .prepare_legacy_graph_plan_multi_sheet(planned)
7244 .map_err(|error| {
7245 ExcelError::new(ExcelErrorKind::Value)
7246 .with_message(format!("target graph preparation failed: {error}"))
7247 })?;
7248 Ok((graph, formula_count))
7249 }
7250
7251 fn replay_target_coordinates(
7252 &mut self,
7253 replay: &Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
7254 coordinates: &[(u32, u32)],
7255 deadline: Option<std::time::Instant>,
7256 scratch: &mut u64,
7257 ) -> Result<Option<Vec<crate::engine::DeferredReplayFormula>>, ExcelError> {
7258 if coordinates.is_empty() {
7259 return Ok(Some(Vec::new()));
7260 }
7261 let mut guard = replay.lock().map_err(|_| {
7262 ExcelError::new(ExcelErrorKind::Value)
7263 .with_message("deferred formula spool lock poisoned")
7264 })?;
7265 let retained = guard.selection_cache_footprint().is_some();
7266 let records = guard.replay_selected_exact(coordinates, &mut |work, bytes| {
7267 self.target_preparation_checkpoint(deadline, work)?;
7268 if retained && let Some(ledger) = self.active_resource_ledger.as_mut() {
7269 ledger
7270 .reserve_retained(bytes)
7271 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
7272 self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
7273 }
7274 self.reserve_graph_source_scratch(bytes)?;
7275 *scratch = scratch.saturating_add(bytes);
7276 Ok(())
7277 });
7278 drop(guard);
7279 let reconciled = self.reconcile_source_cache_footprints();
7280 let records = records?;
7281 reconciled?;
7282 Ok(records)
7283 }
7284
7285 fn prepare_target_exact_source_selection(
7286 &mut self,
7287 sheet: &str,
7288 lease: StagedPackageLease,
7289 coordinates: Vec<(u32, u32)>,
7290 previous: &BTreeSet<(u32, u32)>,
7291 allow_partial_shared: bool,
7292 deadline: Option<std::time::Instant>,
7293 scratch: &mut u64,
7294 ) -> Result<Option<PreparedTargetSourcePackage>, ExcelError> {
7295 let package = self
7296 .staged_formulas
7297 .get(sheet)
7298 .and_then(|s| s.deferred_package.as_ref())
7299 .unwrap();
7300 if !package.source_geometry_complete
7301 || ((!package.families.is_empty() || !package.partitioned_families.is_empty())
7302 && !package.coordinates_cover_families)
7303 || package.reconciliation_replay.is_some()
7304 {
7305 return Ok(None);
7306 }
7307 let selected_points: BTreeSet<_> = coordinates
7308 .into_iter()
7309 .filter(|point| !package.suppressed.contains(point))
7310 .collect();
7311 let mut prepared = PreparedTargetSourcePackage::empty_selection(
7312 sheet,
7313 self.graph.sheet_id(sheet).unwrap(),
7314 lease,
7315 );
7316 if selected_points.is_empty() {
7317 return Ok(Some(prepared));
7318 }
7319 let mut routing = crate::engine::FormulaReplayDisposition::default();
7320 for partition in &package.partitioned_families {
7321 routing
7322 .register_partition(partition, false)
7323 .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?;
7324 }
7325 routing.extend_suppressed_excel_coords(package.suppressed.iter().copied());
7326 routing.register_consumed_members(
7327 package.consumed_engine.as_ref(),
7328 package.consumed_members.iter().copied(),
7329 );
7330 let replay = Arc::clone(&package.replay);
7331 let source_report = package.accounting_report();
7332 prepared.source_report = source_report;
7333 prepared.disposition = routing.clone();
7334
7335 let replay_points: BTreeSet<_> = selected_points
7336 .iter()
7337 .copied()
7338 .filter(|&(row, col)| !prepared.direct_contains(row, col))
7339 .collect();
7340 let coordinates: Vec<_> = replay_points.iter().copied().collect();
7341 let records = self.replay_target_coordinates(&replay, &coordinates, deadline, scratch)?;
7342 prepared.spool_replays = u64::from(!coordinates.is_empty());
7343 let Some(mut replay_records) = records else {
7344 return Ok(None);
7345 };
7346 for record in &mut replay_records {
7347 if record.family.is_none() {
7348 record.partition_owner = routing
7349 .ordinary_disposition(crate::engine::SourceCoord {
7350 row: record.row.saturating_sub(1),
7351 col: record.col.saturating_sub(1),
7352 })
7353 .1;
7354 }
7355 }
7356 replay_records.sort_by_key(|record| record.source_order);
7357 if replay_records
7358 .iter()
7359 .any(|record| !selected_points.contains(&(record.row, record.col)))
7360 || replay_records
7361 .windows(2)
7362 .any(|records| records[0].source_order == records[1].source_order)
7363 {
7364 return Err(ExcelError::new(ExcelErrorKind::Value)
7365 .with_message("invalid indexed exact source selection"));
7366 }
7367 let represented: BTreeSet<_> = replay_records.iter().map(|r| (r.row, r.col)).collect();
7368 if represented != replay_points {
7369 return Err(ExcelError::new(ExcelErrorKind::Value)
7370 .with_message("incomplete indexed exact source selection"));
7371 }
7372 let source = self
7375 .staged_formulas
7376 .get(sheet)
7377 .unwrap()
7378 .deferred_package
7379 .as_ref()
7380 .unwrap();
7381 let contains = |rect: crate::engine::SourceRect, coord: crate::engine::SourceCoord| {
7382 coord.row >= rect.start.row
7383 && coord.row <= rect.end.row
7384 && coord.col >= rect.start.col
7385 && coord.col <= rect.end.col
7386 };
7387 let mut checked = BTreeSet::new();
7388 for record in replay_records.iter().rev() {
7389 if !checked.insert((record.row, record.col)) {
7390 continue;
7391 }
7392 let coord = crate::engine::SourceCoord {
7393 row: record.row - 1,
7394 col: record.col - 1,
7395 };
7396 let agrees = |owner, shared: bool| {
7397 record.family == shared.then_some(owner)
7398 && record.partition_owner.or(record.family) == Some(owner)
7399 };
7400 let mut valid = true;
7401 for family in &source.families {
7402 let owns = match &family.members {
7403 crate::engine::SourceFamilyMembers::CompleteDomain(domain) => {
7404 contains(domain.rect(), coord)
7405 }
7406 crate::engine::SourceFamilyMembers::ExplicitMembers(members) => {
7407 members.as_slice().binary_search(&coord).is_ok()
7408 }
7409 };
7410 if owns && !agrees(family.source_id, true) {
7411 valid = false;
7412 }
7413 }
7414 for family in &source.partitioned_families {
7415 if family
7416 .fragments
7417 .iter()
7418 .any(|fragment| contains(fragment.rect(), coord))
7419 && !agrees(family.source_id, true)
7420 {
7421 valid = false;
7422 }
7423 if let Some(member) = family
7424 .legacy_members
7425 .as_slice()
7426 .iter()
7427 .find(|member| member.coord == coord)
7428 && !agrees(
7429 family.source_id,
7430 member.kind == crate::engine::PartitionLegacyMemberKind::SharedFamilyMember,
7431 )
7432 {
7433 valid = false;
7434 }
7435 }
7436 if !valid {
7437 return Err(ExcelError::new(ExcelErrorKind::Value)
7438 .with_message("indexed source ownership mismatch"));
7439 }
7440 }
7441 prepared.selected_points = Some(selected_points);
7442 prepared.replay_records = replay_records;
7443 Ok(Some(prepared))
7444 }
7445
7446 fn prepare_target_source_package(
7447 &mut self,
7448 sheet: &str,
7449 lease: StagedPackageLease,
7450 deadline: Option<std::time::Instant>,
7451 ) -> Result<PreparedTargetSourcePackage, ExcelError> {
7452 let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
7453 ExcelError::new(ExcelErrorKind::Ref)
7454 .with_message(format!("deferred source sheet not found: {sheet}"))
7455 })?;
7456 let (
7457 source_report,
7458 families,
7459 partitions,
7460 replay,
7461 invalidated,
7462 suppressed,
7463 consumed_members,
7464 consumed_engine,
7465 reconciliation_replay,
7466 ) = {
7467 let package = self
7468 .staged_formulas
7469 .get(sheet)
7470 .and_then(|staged| staged.deferred_package.as_ref())
7471 .ok_or_else(|| {
7472 ExcelError::new(ExcelErrorKind::Value)
7473 .with_message("staged deferred source package is unavailable")
7474 })?;
7475 if package.sheet_name != sheet {
7476 return Err(ExcelError::new(ExcelErrorKind::Value)
7477 .with_message("deferred formula package sheet mismatch"));
7478 }
7479 (
7480 package.accounting_report(),
7481 package.families.clone(),
7482 package.partitioned_families.clone(),
7483 Arc::clone(&package.replay),
7484 package.invalidated.clone(),
7485 package.suppressed.clone(),
7486 package.consumed_members.clone(),
7487 package.consumed_engine.clone(),
7488 package.reconciliation_replay.clone(),
7489 )
7490 };
7491
7492 let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
7493 for partition in &partitions {
7494 replay_disposition
7495 .register_partition(partition, false)
7496 .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
7497 }
7498 replay_disposition.extend_suppressed_excel_coords(suppressed.iter().copied());
7499 replay_disposition
7500 .register_consumed_members(consumed_engine.as_ref(), consumed_members.iter().copied());
7501 if !replay_disposition.consumed_engine_matches(&self.source_formula_token) {
7502 return Err(ExcelError::new(ExcelErrorKind::Value)
7503 .with_message("ResidualConsumedEngineMismatch"));
7504 }
7505 self.target_preparation_checkpoint(deadline, 1)?;
7506 let mut replay_records = if let Some(mut records) = reconciliation_replay {
7507 records.retain(|record| {
7508 let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1))
7509 else {
7510 return true;
7511 };
7512 let coord = crate::engine::SourceCoord { row, col };
7513 let disposition = record.family.map_or_else(
7514 || replay_disposition.ordinary_disposition(coord).0,
7515 |family| replay_disposition.shared_disposition(family, coord),
7516 );
7517 !matches!(
7518 disposition,
7519 crate::engine::FormulaReplayCoordinateDisposition::Direct
7520 | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
7521 )
7522 });
7523 records
7524 } else {
7525 replay
7526 .lock()
7527 .map_err(|_| {
7528 ExcelError::new(ExcelErrorKind::Value)
7529 .with_message("deferred formula spool lock poisoned")
7530 })?
7531 .replay_partitioned(&replay_disposition, &partitions)
7532 .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?
7533 };
7534 replay_records.sort_by_key(|record| record.source_order);
7535 for chunk in replay_records.chunks(256) {
7536 self.target_preparation_checkpoint(deadline, chunk.len() as u64)?;
7537 }
7538 if replay_records
7539 .windows(2)
7540 .any(|records| records[0].source_order == records[1].source_order)
7541 {
7542 return Err(ExcelError::new(ExcelErrorKind::Value)
7543 .with_message("duplicate deferred source-order proof"));
7544 }
7545
7546 let disposition = replay_disposition;
7547 let direct_families = 0usize;
7548 let direct_cells = 0u64;
7549 let direct_fragments = 0u64;
7550 let direct_complete_families = 0u64;
7551 let direct_complete_cells = 0u64;
7552 let direct_partition_families = 0u64;
7553 let direct_partition_cells = 0u64;
7554 let anchor_parses = 0u64;
7555 let anchor_asts = 0u64;
7556 let anchor_analyses = 0u64;
7557
7558 Ok(PreparedTargetSourcePackage {
7559 sheet: sheet.to_string(),
7560 sheet_id,
7561 lease,
7562 selected_points: None,
7563 complete_selections: Default::default(),
7564 deferred_shared: false,
7565 direct_domains: Vec::new(),
7566 source_report,
7567 replay_records,
7568 spool_replays: 1,
7569 disposition,
7570 legacy: Vec::new(),
7571 direct_families,
7572 direct_cells,
7573 direct_fragments,
7574 direct_complete_families,
7575 direct_complete_cells,
7576 direct_partition_families,
7577 direct_partition_cells,
7578 anchor_parses,
7579 anchor_asts,
7580 anchor_analyses,
7581 })
7582 }
7583
7584 fn fallback_planning_snapshot(
7585 &self,
7586 batch: &FormulaIngestBatch,
7587 ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
7588 let mut requests = Vec::new();
7589 for formula in &batch.formulas {
7590 let ast = self
7591 .graph
7592 .data_store()
7593 .retrieve_ast(formula.ast_id, self.graph.sheet_reg())
7594 .ok_or_else(|| {
7595 ExcelError::new(ExcelErrorKind::Value)
7596 .with_message("ordered fallback AST is unavailable")
7597 })?;
7598 Self::collect_planning_function_requests(&ast, &mut requests);
7599 }
7600 requests.sort();
7601 requests.dedup();
7602 crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
7603 &self.resolver,
7604 requests,
7605 )
7606 .map_err(|error| ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}")))
7607 }
7608
7609 fn prepare_legacy_batch_fallback(
7610 &mut self,
7611 batch: FormulaIngestBatch,
7612 function_provider: &dyn crate::traits::FunctionProvider,
7613 ) -> Result<
7614 (
7615 crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan,
7616 u64,
7617 ),
7618 ExcelError,
7619 > {
7620 let formula_count = batch.formulas.len() as u64;
7621 let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
7622 let mut planned = Vec::with_capacity(batch.formulas.len());
7623 for record in batch.formulas {
7624 let placement = CellRef::new(
7625 sheet_id,
7626 Coord::from_excel(record.row, record.col, true, true),
7627 );
7628 let ingested = self
7629 .graph
7630 .ingest_pipeline(function_provider)
7631 .enable_function_semantics()
7632 .ingest_formula(
7633 FormulaAstInput::RawArena(record.ast_id),
7634 placement,
7635 record.formula_text,
7636 )
7637 .map_err(|error| {
7638 ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}"))
7639 })?;
7640 planned.push((record.row, record.col, ingested.ast_id, ingested.dep_plan));
7641 }
7642 let plan = self
7643 .graph
7644 .prepare_legacy_graph_plan(sheet_id, planned)
7645 .map_err(|error| {
7646 ExcelError::new(ExcelErrorKind::Value).with_message(error.to_string())
7647 })?;
7648 Ok((plan, formula_count))
7649 }
7650
7651 fn publish_compressed_partial_report(
7652 &mut self,
7653 report: &FormulaIngestReport,
7654 direct_report: &FormulaIngestReport,
7655 ) {
7656 if direct_report.source_family_promoted == 0
7657 && direct_report.graph_formula_cells_materialized == 0
7658 {
7659 return;
7660 }
7661 let mut published = report.clone();
7662 published.accumulate(direct_report);
7663 self.record_formula_ingest_report(published);
7664 }
7665
7666 fn finish_compressed_formula_sources(
7667 &mut self,
7668 batches: Vec<(
7669 FormulaIngestBatch,
7670 crate::engine::FormulaCompressedSourceReport,
7671 crate::engine::FormulaCompressedPreparation,
7672 )>,
7673 ) -> Result<FormulaIngestReport, ExcelError> {
7674 self.observe_function_semantic_epoch()?;
7675 if batches.iter().any(|(_, _, preparation)| {
7676 !Arc::ptr_eq(&preparation.engine_token, &self.source_formula_token)
7677 }) {
7678 return Err(ExcelError::new(ExcelErrorKind::Value)
7679 .with_message("compressed source preparation belongs to another engine"));
7680 }
7681 if batches.iter().any(|(fallback, _, preparation)| {
7682 preparation.sheet_name.as_ref() != fallback.sheet_name
7683 }) {
7684 return Err(ExcelError::new(ExcelErrorKind::Value)
7685 .with_message("compressed source preparation sheet mismatch"));
7686 }
7687 let initial_guard = crate::function_registry::semantic_epoch_read_guard();
7688 let initial_provider_revision = self.resolver.planning_semantic_revision();
7689 let mut fallback_batches = Vec::with_capacity(batches.len());
7690 let mut stale_fallback_batches = Vec::new();
7691 let mut pending_preparations = Vec::new();
7692 for (mut fallback, mut source, mut preparation) in batches {
7693 for formula in fallback.formulas.drain(..) {
7694 let source_order = formula.source_order.ok_or_else(|| {
7695 ExcelError::new(ExcelErrorKind::Value).with_message(
7696 "compressed source supplied formulas without source-order proof",
7697 )
7698 })?;
7699 let text = formula.formula_text.ok_or_else(|| {
7700 ExcelError::new(ExcelErrorKind::Value).with_message(
7701 "ordered compressed fallback formula has no exact source text",
7702 )
7703 })?;
7704 preparation
7705 .eager_replay
7706 .push(crate::engine::DeferredReplayFormula {
7707 source_order,
7708 row: formula.row,
7709 col: formula.col,
7710 text: text.to_string(),
7711 family: formula.source_family,
7712 partition_owner: formula.partition_owner,
7713 });
7714 }
7715 preparation
7716 .eager_replay
7717 .sort_by_key(|record| record.source_order);
7718 source.source_spool_replays = source
7719 .source_spool_replays
7720 .saturating_add(preparation.preparation_spool_replays);
7721 let stale_reason = preparation
7722 .function_semantics_used
7723 .then(|| {
7724 if preparation.function_provider_revision != initial_provider_revision {
7725 Some("FunctionProviderRevisionChanged")
7726 } else if self.prepared_function_semantics_changed(&preparation, &initial_guard)
7727 {
7728 Some("FunctionSemanticEpochChanged")
7729 } else {
7730 None
7731 }
7732 })
7733 .flatten();
7734 let stale_semantics = stale_reason.is_some();
7735 for reason in preparation.rejected.values() {
7736 *source.fallback_reasons.entry(reason.clone()).or_default() += 1;
7737 }
7738 source.replay_families = source.families_seen;
7740 source.replay_cells = source.family_cells_seen;
7741 let compressed = crate::engine::FormulaCompressedSourceBatch::new(
7742 fallback.sheet_name.clone(),
7743 source,
7744 );
7745 if stale_semantics {
7746 stale_fallback_batches.push((fallback, compressed));
7747 } else {
7748 fallback_batches.push((fallback, compressed));
7749 }
7750 pending_preparations.push((preparation, stale_semantics));
7751 }
7752 drop(initial_guard);
7753
7754 let configured_mode = self.config.formula_plane_mode;
7756 self.config.formula_plane_mode = FormulaPlaneMode::Off;
7757 let stale_result =
7758 self.ingest_compressed_formula_source_batches_inner(stale_fallback_batches, false);
7759 self.config.formula_plane_mode = configured_mode;
7760 let mut report = stale_result?;
7761 report.mode = configured_mode;
7762
7763 self.config.formula_plane_mode = FormulaPlaneMode::Off;
7764 let fallback_result =
7765 self.ingest_compressed_formula_source_batches_inner(fallback_batches, false);
7766 self.config.formula_plane_mode = configured_mode;
7767 match fallback_result {
7768 Ok(fallback_report) => report.accumulate(&fallback_report),
7769 Err(error) => {
7770 self.record_formula_ingest_report(report);
7771 return Err(error);
7772 }
7773 }
7774
7775 let mut direct_report =
7776 FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
7777 for (preparation, _) in &pending_preparations {
7778 direct_report.source_anchor_parses = direct_report.source_anchor_parses.saturating_add(
7779 preparation.clean_rejected_anchor_counts[0]
7780 .saturating_add(preparation.fragmented_rejected_anchor_counts[0]),
7781 );
7782 direct_report.source_anchor_asts = direct_report.source_anchor_asts.saturating_add(
7783 preparation.clean_rejected_anchor_counts[1]
7784 .saturating_add(preparation.fragmented_rejected_anchor_counts[1]),
7785 );
7786 direct_report.source_anchor_analyses =
7787 direct_report.source_anchor_analyses.saturating_add(
7788 preparation.clean_rejected_anchor_counts[2]
7789 .saturating_add(preparation.fragmented_rejected_anchor_counts[2]),
7790 );
7791 }
7792 loop {
7793 #[cfg(any(test, feature = "test-support"))]
7794 if let Some(hook) = self.before_prepared_span_commit_hook.take() {
7795 hook();
7796 }
7797 let commit_guard = crate::function_registry::semantic_epoch_read_guard();
7798 let commit_provider_revision = self.resolver.planning_semantic_revision();
7799 let mut newly_stale = Vec::new();
7800 let mut current = Vec::new();
7801 for pending in pending_preparations.drain(..) {
7802 let stale_reason = pending
7803 .0
7804 .function_semantics_used
7805 .then(|| {
7806 if pending.0.function_provider_revision != commit_provider_revision {
7807 Some("FunctionProviderRevisionChanged")
7808 } else if self
7809 .prepared_function_semantics_changed(&pending.0, &commit_guard)
7810 {
7811 Some("FunctionSemanticEpochChanged")
7812 } else {
7813 None
7814 }
7815 })
7816 .flatten();
7817 if let Some(reason) = stale_reason {
7818 newly_stale.push((pending, reason));
7819 } else {
7820 current.push(pending);
7821 }
7822 }
7823 if !newly_stale.is_empty() {
7824 drop(commit_guard);
7825 for ((mut preparation, was_initially_stale), reason) in newly_stale {
7826 preparation
7827 .eager_replay
7828 .sort_by_key(|record| record.source_order);
7829 direct_report.source_spool_replays =
7830 direct_report.source_spool_replays.saturating_add(1);
7831 if !was_initially_stale {
7832 direct_report
7833 .fallback_reasons
7834 .entry(reason.to_string())
7835 .or_default();
7836 }
7837 preparation.function_semantics_used = false;
7838 if !preparation.eager_replay.is_empty() {
7839 current.push((preparation, false));
7840 }
7841 }
7842 pending_preparations = current;
7843 continue;
7844 }
7845 drop(commit_guard);
7846
7847 enum SourceProposal {
7849 KnownFallback {
7850 source_order: crate::engine::SourceFormulaOrder,
7851 records: Vec<crate::engine::DeferredReplayFormula>,
7852 },
7853 }
7854
7855 impl SourceProposal {
7856 fn source_order(&self) -> crate::engine::SourceFormulaOrder {
7857 match self {
7858 Self::KnownFallback { source_order, .. } => *source_order,
7859 }
7860 }
7861 }
7862
7863 for (mut preparation, _) in current {
7864 let mut proposals = Vec::with_capacity(preparation.eager_replay.len());
7865 let mut family_fallbacks: BTreeMap<_, Vec<_>> = BTreeMap::new();
7866 for record in preparation.eager_replay.drain(..) {
7867 if let Some(owner) = record.partition_owner.or(record.family) {
7868 family_fallbacks.entry(owner).or_default().push(record);
7869 } else {
7870 proposals.push(SourceProposal::KnownFallback {
7871 source_order: record.source_order,
7872 records: vec![record],
7873 });
7874 }
7875 }
7876 for (_, mut records) in family_fallbacks {
7877 records.sort_by_key(|record| record.source_order);
7878 if records
7879 .windows(2)
7880 .any(|window| window[0].source_order == window[1].source_order)
7881 {
7882 self.publish_compressed_partial_report(&report, &direct_report);
7883 return Err(ExcelError::new(ExcelErrorKind::Value)
7884 .with_message("duplicate exact-replay source-order proof"));
7885 }
7886 let Some(source_order) = records.first().map(|record| record.source_order)
7887 else {
7888 self.publish_compressed_partial_report(&report, &direct_report);
7889 return Err(ExcelError::new(ExcelErrorKind::Value)
7890 .with_message("empty exact-replay fallback family"));
7891 };
7892 proposals.push(SourceProposal::KnownFallback {
7893 source_order,
7894 records,
7895 });
7896 }
7897 proposals.sort_by_key(SourceProposal::source_order);
7898 if proposals
7899 .windows(2)
7900 .any(|window| window[0].source_order() == window[1].source_order())
7901 {
7902 self.publish_compressed_partial_report(&report, &direct_report);
7903 return Err(ExcelError::new(ExcelErrorKind::Value)
7904 .with_message("ambiguous compressed source-order proof"));
7905 }
7906
7907 for proposal in proposals {
7908 match proposal {
7909 SourceProposal::KnownFallback { records, .. } => {
7910 let batch = match self.formula_batch_from_exact_replay(
7911 preparation.sheet_name.as_ref(),
7912 records,
7913 ) {
7914 Ok(batch) => batch,
7915 Err(error) => {
7916 self.publish_compressed_partial_report(&report, &direct_report);
7917 return Err(error);
7918 }
7919 };
7920 if batch.is_empty() {
7921 continue;
7922 }
7923 let snapshot = match self.fallback_planning_snapshot(&batch) {
7924 Ok(snapshot) => snapshot,
7925 Err(error) => {
7926 self.publish_compressed_partial_report(&report, &direct_report);
7927 return Err(error);
7928 }
7929 };
7930 let commit_guard =
7931 crate::function_registry::semantic_epoch_read_guard();
7932 let provider_revision_initial =
7933 self.resolver.planning_semantic_revision();
7934 if (commit_guard.epoch() != snapshot.epoch()
7935 && snapshot.semantic_changes_affect_requests_since_guarded(
7936 &commit_guard,
7937 snapshot.epoch(),
7938 ))
7939 || snapshot.provider_revision().is_some_and(|revision| {
7940 Some(revision) != provider_revision_initial
7941 })
7942 {
7943 drop(commit_guard);
7944 self.publish_compressed_partial_report(&report, &direct_report);
7945 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7946 "ordered fallback planning snapshot became stale",
7947 ));
7948 }
7949 let (plan, formula_count) = match self
7950 .prepare_legacy_batch_fallback(batch, &snapshot)
7951 {
7952 Ok(plan) => plan,
7953 Err(error) => {
7954 drop(commit_guard);
7955 self.publish_compressed_partial_report(&report, &direct_report);
7956 return Err(error);
7957 }
7958 };
7959 let provider_revision_after =
7960 self.resolver.planning_semantic_revision();
7961 if provider_revision_after != provider_revision_initial {
7962 drop(commit_guard);
7963 self.publish_compressed_partial_report(&report, &direct_report);
7964 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7965 "function provider changed while preparing ordered fallback",
7966 ));
7967 }
7968 let graph_vertices = plan.new_vertex_count();
7969 let Some(graph_edges) = plan.planned_edge_count() else {
7970 drop(commit_guard);
7971 self.publish_compressed_partial_report(&report, &direct_report);
7972 return Err(ExcelError::new(ExcelErrorKind::Value)
7973 .with_message("prepared ordered fallback size overflow"));
7974 };
7975 if let Err(error) = self.prepared_legacy_admission(&plan, formula_count)
7976 {
7977 drop(commit_guard);
7978 self.publish_compressed_partial_report(&report, &direct_report);
7979 return Err(error);
7980 }
7981 if let Err(error) =
7982 self.graph.validate_prepared_legacy_graph_plan(&plan)
7983 {
7984 drop(commit_guard);
7985 self.publish_compressed_partial_report(&report, &direct_report);
7986 return Err(ExcelError::new(ExcelErrorKind::Value)
7987 .with_message(error.to_string()));
7988 }
7989 #[cfg(test)]
7990 if let Some(hook) = self
7991 .before_legacy_fallback_final_provider_sample_hook
7992 .take()
7993 {
7994 hook();
7995 }
7996 let provider_revision_final =
7997 self.resolver.planning_semantic_revision();
7998 if provider_revision_final != provider_revision_initial {
7999 drop(commit_guard);
8000 self.publish_compressed_partial_report(&report, &direct_report);
8001 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8002 "function provider changed after ordered fallback validation",
8003 ));
8004 }
8005 let graph_formulas =
8006 self.graph.apply_prevalidated_legacy_graph_plan(plan);
8007 direct_report.formula_cells_seen = direct_report
8008 .formula_cells_seen
8009 .saturating_add(formula_count);
8010 direct_report.graph_formula_cells_materialized = direct_report
8011 .graph_formula_cells_materialized
8012 .saturating_add(graph_formulas as u64);
8013 direct_report.graph_vertices_created = direct_report
8014 .graph_vertices_created
8015 .saturating_add(graph_vertices as u64);
8016 direct_report.graph_edges_created = direct_report
8017 .graph_edges_created
8018 .saturating_add(graph_edges as u64);
8019 }
8020 }
8021 #[cfg(test)]
8022 if let Some(hook) = self.after_eager_proposal_commit_hook.take() {
8023 hook();
8024 }
8025 }
8026 }
8027 break;
8028 }
8029 report.accumulate(&direct_report);
8030 self.record_formula_ingest_report(report.clone());
8031 Ok(report)
8032 }
8033 pub(crate) fn ingest_compressed_formula_source_batches(
8035 &mut self,
8036 batches: Vec<(
8037 FormulaIngestBatch,
8038 crate::engine::FormulaCompressedSourceBatch,
8039 )>,
8040 ) -> Result<FormulaIngestReport, ExcelError> {
8041 self.ingest_compressed_formula_source_batches_inner(batches, true)
8042 }
8043
8044 fn ingest_compressed_formula_source_batches_inner(
8045 &mut self,
8046 batches: Vec<(
8047 FormulaIngestBatch,
8048 crate::engine::FormulaCompressedSourceBatch,
8049 )>,
8050 publish_report: bool,
8051 ) -> Result<FormulaIngestReport, ExcelError> {
8052 let mut source_counts = [0_u64; 11];
8053 let mut source_report = crate::engine::FormulaCompressedSourceReport::default();
8054 let mut formula_batches = Vec::with_capacity(batches.len());
8055 let mut compressed_families = Vec::new();
8056 let mut partitioned_families = Vec::new();
8057 for (batch, compressed_batch) in batches {
8058 let (sheet_name, compressed, families, partitions) = compressed_batch.into_parts();
8059 if sheet_name.as_ref() != batch.sheet_name {
8060 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8061 "compressed formula source sheet does not match its replay batch",
8062 ));
8063 }
8064 compressed_families.push((batch.sheet_name.clone(), families));
8065 partitioned_families.push((batch.sheet_name.clone(), partitions));
8066 source_counts[0] = source_counts[0].saturating_add(compressed.source_formula_events);
8067 source_counts[1] = source_counts[1].saturating_add(compressed.source_ordinary_events);
8068 source_counts[2] =
8069 source_counts[2].saturating_add(compressed.source_shared_anchor_events);
8070 source_counts[3] =
8071 source_counts[3].saturating_add(compressed.source_shared_descendant_events);
8072 source_counts[4] = source_counts[4].saturating_add(compressed.source_unknown_events);
8073 source_counts[5] =
8074 source_counts[5].saturating_add(compressed.source_formula_records_spooled);
8075 source_counts[6] =
8076 source_counts[6].saturating_add(compressed.source_spool_encoded_bytes);
8077 source_counts[7] = source_counts[7].max(compressed.source_spool_peak_memory_bytes);
8078 source_counts[8] =
8079 source_counts[8].saturating_add(compressed.source_spool_spilled_bytes);
8080 source_counts[9] = source_counts[9].saturating_add(compressed.source_spool_spill_files);
8081 source_counts[10] = source_counts[10].saturating_add(compressed.source_spool_replays);
8082 source_report.families_seen = source_report
8083 .families_seen
8084 .saturating_add(compressed.families_seen);
8085 source_report.family_cells_seen = source_report
8086 .family_cells_seen
8087 .saturating_add(compressed.family_cells_seen);
8088 source_report.source_clean_families = source_report
8089 .source_clean_families
8090 .saturating_add(compressed.source_clean_families);
8091 source_report.source_clean_cells = source_report
8092 .source_clean_cells
8093 .saturating_add(compressed.source_clean_cells);
8094 source_report.source_fragmentable_families = source_report
8095 .source_fragmentable_families
8096 .saturating_add(compressed.source_fragmentable_families);
8097 source_report.source_fragmentable_cells = source_report
8098 .source_fragmentable_cells
8099 .saturating_add(compressed.source_fragmentable_cells);
8100 source_report.source_fragment_count = source_report
8101 .source_fragment_count
8102 .saturating_add(compressed.source_fragment_count);
8103 source_report.source_isolated_fallback_cells = source_report
8104 .source_isolated_fallback_cells
8105 .saturating_add(compressed.source_isolated_fallback_cells);
8106 source_report.source_hole_exclusions = source_report
8107 .source_hole_exclusions
8108 .saturating_add(compressed.source_hole_exclusions);
8109 source_report.source_ordinary_exclusions = source_report
8110 .source_ordinary_exclusions
8111 .saturating_add(compressed.source_ordinary_exclusions);
8112 source_report.source_partition_failures = source_report
8113 .source_partition_failures
8114 .saturating_add(compressed.source_partition_failures);
8115 source_report.replay_families = source_report
8116 .replay_families
8117 .saturating_add(compressed.replay_families);
8118 source_report.replay_cells = source_report
8119 .replay_cells
8120 .saturating_add(compressed.replay_cells);
8121 source_report.forward_descendants = source_report
8122 .forward_descendants
8123 .saturating_add(compressed.forward_descendants);
8124 source_report.evidence_limit_fallbacks = source_report
8125 .evidence_limit_fallbacks
8126 .saturating_add(compressed.evidence_limit_fallbacks);
8127 source_report.evidence_peak_bytes = source_report
8128 .evidence_peak_bytes
8129 .max(compressed.evidence_peak_bytes);
8130 for (reason, count) in compressed.fallback_reasons {
8131 *source_report.fallback_reasons.entry(reason).or_default() += count;
8132 }
8133 formula_batches.push(batch);
8134 }
8135 self.ingest_formula_batches_inner(
8136 formula_batches,
8137 source_counts,
8138 Some(source_report),
8139 compressed_families,
8140 partitioned_families,
8141 publish_report,
8142 )
8143 }
8144
8145 pub fn ingest_formula_batches(
8146 &mut self,
8147 batches: Vec<FormulaIngestBatch>,
8148 ) -> Result<FormulaIngestReport, ExcelError> {
8149 self.ensure_not_poisoned()?;
8150 let has_formulas = batches.iter().any(|batch| !batch.formulas.is_empty());
8151 let report = self.ingest_formula_batches_inner(
8152 batches,
8153 [0; 11],
8154 None,
8155 Vec::new(),
8156 Vec::new(),
8157 true,
8158 )?;
8159 if has_formulas {
8160 self.mark_topology_edited();
8161 }
8162 Ok(report)
8163 }
8164
8165 fn ingest_formula_batches_unpublished(
8166 &mut self,
8167 batches: Vec<FormulaIngestBatch>,
8168 ) -> Result<FormulaIngestReport, ExcelError> {
8169 self.ingest_formula_batches_inner(batches, [0; 11], None, Vec::new(), Vec::new(), false)
8170 }
8171
8172 fn ingest_formula_batches_inner(
8173 &mut self,
8174 batches: Vec<FormulaIngestBatch>,
8175 source_counts: [u64; 11],
8176 source_report: Option<crate::engine::FormulaCompressedSourceReport>,
8177 compressed_families: Vec<(String, Vec<crate::engine::SourceFormulaFamily>)>,
8178 partitioned_families: Vec<(String, Vec<crate::engine::PartitionedSourceFormulaFamily>)>,
8179 publish_report: bool,
8180 ) -> Result<FormulaIngestReport, ExcelError> {
8181 self.observe_function_semantic_epoch()?;
8182 let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
8183 #[cfg(feature = "tracing")]
8184 let arena_nodes_before = self.graph.data_store().memory_usage().total_ast_nodes;
8185 #[cfg(feature = "tracing")]
8186 let route = if !partitioned_families.is_empty() {
8187 "partitioned"
8188 } else if !compressed_families.is_empty() {
8189 "compressed_source"
8190 } else if source_report.is_some() {
8191 "replay"
8192 } else {
8193 "ordinary"
8194 };
8195 let _ingest_span = crate::engine::trace::fz_span!(
8196 tracing::Level::INFO,
8197 "ingest",
8198 "ingest.batch",
8199 mode = ?FormulaPlaneMode::Off,
8200 route,
8201 formula_cells = formula_cells_seen
8202 );
8203 let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
8204 let materialize_batches = batches;
8205 report.formula_cells_seen = formula_cells_seen;
8206 report.source_formula_events = source_counts[0];
8207 report.source_ordinary_events = source_counts[1];
8208 report.source_shared_anchor_events = source_counts[2];
8209 report.source_shared_descendant_events = source_counts[3];
8210 report.source_unknown_events = source_counts[4];
8211 report.source_formula_records_spooled = source_counts[5];
8212 report.source_spool_encoded_bytes = source_counts[6];
8213 report.source_spool_peak_memory_bytes = source_counts[7];
8214 report.source_spool_spilled_bytes = source_counts[8];
8215 report.source_spool_spill_files = source_counts[9];
8216 report.source_spool_replays = source_counts[10];
8217 if let Some(source) = source_report {
8218 report.source_families_seen = source.families_seen;
8219 report.source_family_cells_seen = source.family_cells_seen;
8220 report.source_family_shadow_eligible = source.source_clean_families;
8221 report.source_family_shadow_eligible_cells = source.source_clean_cells;
8222 report.source_partitioned_families_seen = source.source_fragmentable_families;
8223 report.source_partition_holes = source.source_hole_exclusions;
8224 report.source_partition_ordinary_exceptions = source.source_ordinary_exclusions;
8225 report.source_partition_failures = source.source_partition_failures;
8226 report.source_partition_surviving_cells = source.source_fragmentable_cells;
8227 report.source_family_fallback = report
8228 .source_family_fallback
8229 .saturating_add(source.replay_families);
8230 report.source_family_fallback_cells = report
8231 .source_family_fallback_cells
8232 .saturating_add(source.replay_cells);
8233 report.source_forward_descendants = source.forward_descendants;
8234 report.source_evidence_limit_fallbacks = source.evidence_limit_fallbacks;
8235 report.source_evidence_peak_bytes = source.evidence_peak_bytes;
8236 for (reason, count) in source.fallback_reasons {
8237 let total = report.fallback_reasons.entry(reason).or_default();
8238 *total = total.saturating_add(count);
8239 }
8240 }
8241
8242 if self.graph.first_load_assume_new()
8246 && !self.graph_admission_enabled()
8247 && !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
8248 {
8249 let mut builder =
8250 crate::engine::ingest_builder::BulkIngestBuilder::new(&mut self.graph);
8251 for batch in materialize_batches {
8252 if batch.is_empty() {
8253 continue;
8254 }
8255 let sheet_id = builder
8256 .add_sheet_checked(&batch.sheet_name)
8257 .ok_or_else(|| {
8258 ExcelError::new(ExcelErrorKind::Ref)
8259 .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
8260 })?;
8261 builder.add_formula_refs(
8262 sheet_id,
8263 batch.formulas.into_iter().map(|record| {
8264 (
8265 record.row,
8266 record.col,
8267 crate::engine::graph::FormulaRef::of_ingested(
8268 record.ast_id,
8269 record.member_anchor,
8270 ),
8271 )
8272 }),
8273 );
8274 }
8275 let summary = builder.finish_with_provider(&self.resolver)?;
8276 report.graph_formula_cells_materialized = summary.formulas as u64;
8277 report.graph_vertices_created = summary.vertices as u64;
8278 report.graph_edges_created = summary.edges as u64;
8279 } else if !materialize_batches.iter().all(FormulaIngestBatch::is_empty) {
8280 let mut prepared_by_sheet: BTreeMap<String, Vec<_>> = BTreeMap::new();
8281 for batch in materialize_batches {
8282 if batch.is_empty() {
8283 continue;
8284 }
8285 let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
8286 ExcelError::new(ExcelErrorKind::Ref)
8287 .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
8288 })?;
8289 let mut pipeline = self.ingest_pipeline();
8290 let prepared = prepared_by_sheet.entry(batch.sheet_name).or_default();
8293 prepared.reserve(batch.formulas.len());
8294 for record in batch.formulas {
8295 let placement = CellRef::new(
8296 sheet_id,
8297 Coord::from_excel(record.row, record.col, true, true),
8298 );
8299 let input = match record.member_anchor {
8300 Some(anchor) => FormulaAstInput::Member {
8301 template: record.ast_id,
8302 anchor,
8303 },
8304 None => FormulaAstInput::RawArena(record.ast_id),
8305 };
8306 let formula = pipeline.ingest_formula(input, placement, None)?;
8307 prepared.push((
8308 record.row,
8309 record.col,
8310 crate::engine::graph::FormulaRef::of_ingested(
8311 formula.ast_id,
8312 formula.member_anchor,
8313 ),
8314 formula.dep_plan,
8315 ));
8316 }
8317 }
8318 let admission_preflighted = self.graph_admission_enabled();
8319 if admission_preflighted {
8320 let mut preview = Vec::new();
8321 for (sheet_name, formulas) in &prepared_by_sheet {
8322 let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(|| {
8323 ExcelError::new(ExcelErrorKind::Ref)
8324 .with_message(format!("unknown ingest sheet: {sheet_name}"))
8325 })?;
8326 preview.extend(
8327 formulas
8328 .iter()
8329 .map(|(row, col, _, plan)| (sheet_id, *row, *col, plan.clone())),
8330 );
8331 }
8332 let admission = self.graph.preview_formula_mutations(&preview)?;
8333 self.preflight_graph_admission(admission)?;
8334 }
8335
8336 let mut builder = self.begin_bulk_ingest();
8337 if admission_preflighted {
8338 builder.mark_admission_preflighted();
8339 }
8340 for (sheet_name, formulas) in prepared_by_sheet {
8341 if formulas.is_empty() {
8342 continue;
8343 }
8344 let sheet_id = builder.add_sheet(&sheet_name);
8345 builder.add_formula_plans(sheet_id, formulas);
8346 }
8347 let summary = builder.finish()?;
8348 report.graph_formula_cells_materialized = summary.formulas as u64;
8349 report.graph_vertices_created = summary.vertices as u64;
8350 report.graph_edges_created = summary.edges as u64;
8351 }
8352
8353 crate::engine::trace::fz_event!(
8354 tracing::Level::INFO,
8355 "ingest",
8356 "ingest.summary",
8357 candidate_cells = report.shadow_candidate_cells,
8358 accepted_span_cells = report.shadow_accepted_span_cells,
8359 fallback_cells = report.shadow_fallback_cells,
8360 spans_created = report.shadow_spans_created,
8361 templates_interned = report.shadow_templates_interned,
8362 graph_vertices_created = report.graph_vertices_created,
8363 graph_edges_created = report.graph_edges_created,
8364 arena_nodes_delta = self
8365 .graph
8366 .data_store()
8367 .memory_usage()
8368 .total_ast_nodes
8369 .saturating_sub(arena_nodes_before)
8370 );
8371 if publish_report {
8372 self.record_formula_ingest_report(report.clone());
8373 }
8374 Ok(report)
8375 }
8376
8377 fn dedup_formula_parse_diagnostics_since(&mut self, start: usize) {
8378 let mut unique = Vec::new();
8379 for diagnostic in self.formula_parse_diagnostics.drain(start..) {
8380 let duplicate = unique.iter().any(|prior: &FormulaParseDiagnostic| {
8381 prior.sheet == diagnostic.sheet
8382 && prior.row == diagnostic.row
8383 && prior.col == diagnostic.col
8384 && prior.formula == diagnostic.formula
8385 && prior.policy == diagnostic.policy
8386 });
8387 if !duplicate {
8388 unique.push(diagnostic);
8389 }
8390 }
8391 self.formula_parse_diagnostics.extend(unique);
8392 }
8393
8394 pub fn handle_formula_parse_error(
8395 &mut self,
8396 sheet: &str,
8397 row: u32,
8398 col: u32,
8399 formula: &str,
8400 message: String,
8401 ) -> Result<Option<ASTNode>, ExcelError> {
8402 self.ensure_not_poisoned()?;
8403 let policy = self.config.formula_parse_policy;
8404
8405 if policy == FormulaParsePolicy::Strict {
8406 let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
8407 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
8408 "Formula parse error at {sheet}!{col_a1}{row}: {message}"
8409 )));
8410 }
8411
8412 self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
8413 sheet: sheet.to_string(),
8414 row,
8415 col,
8416 formula: formula.to_string(),
8417 message: message.clone(),
8418 policy,
8419 });
8420
8421 match policy {
8422 FormulaParsePolicy::Strict => unreachable!(),
8423 FormulaParsePolicy::KeepCachedValue => Ok(None),
8424 FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
8425 ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
8426 None,
8427 ))),
8428 FormulaParsePolicy::CoerceToError => {
8429 let err = ExcelError::new(ExcelErrorKind::Error)
8430 .with_message(format!("Malformed formula: {message}"));
8431 Ok(Some(ASTNode::new(
8432 ASTNodeType::Literal(LiteralValue::Error(err)),
8433 None,
8434 )))
8435 }
8436 }
8437 }
8438
8439 #[cfg(test)]
8440 fn target_preparation_fault(
8441 &mut self,
8442 seam: crate::engine::target_preparation::TargetPreparationFault,
8443 ) -> Result<(), ExcelError> {
8444 if self.target_preparation_fault_for_test == Some(seam) {
8445 self.target_preparation_fault_for_test = None;
8446 Err(ExcelError::new(ExcelErrorKind::Value)
8447 .with_message(format!("injected target preparation fault: {seam:?}")))
8448 } else {
8449 Ok(())
8450 }
8451 }
8452
8453 fn preparation_stale(
8454 reason: formualizer_common::PreparationStaleReason,
8455 message: impl Into<String>,
8456 ) -> ExcelError {
8457 ExcelError::new(ExcelErrorKind::Value)
8458 .with_message(message)
8459 .with_extra(formualizer_common::ExcelErrorExtra::PreparationStale { reason })
8460 }
8461
8462 fn preparation_revision_stale_reason(
8463 assumptions: &crate::engine::PreparationRevision,
8464 current: &crate::engine::PreparationRevision,
8465 planning_requests: &BTreeSet<(String, String, usize)>,
8466 staged_leases_match: bool,
8467 ) -> Option<formualizer_common::PreparationStaleReason> {
8468 if assumptions.graph != current.graph {
8469 Some(formualizer_common::PreparationStaleReason::Graph)
8470 } else if assumptions.staged != current.staged || !staged_leases_match {
8471 Some(formualizer_common::PreparationStaleReason::Staged)
8472 } else if assumptions.symbols != current.symbols {
8473 Some(formualizer_common::PreparationStaleReason::Symbols)
8474 } else if assumptions.provider != current.provider {
8475 Some(formualizer_common::PreparationStaleReason::Provider)
8476 } else if assumptions.semantic != current.semantic
8477 && crate::function_registry::semantic_changes_affect_requests_since(
8478 assumptions.semantic,
8479 planning_requests.iter().cloned(),
8480 )
8481 {
8482 Some(formualizer_common::PreparationStaleReason::Semantic)
8483 } else {
8484 None
8485 }
8486 }
8487
8488 fn preparation_revisions(&self) -> crate::engine::PreparationRevision {
8489 crate::engine::PreparationRevision {
8490 graph: self.graph.topology_revision(),
8491 authority: 0,
8492 authority_indexes: 0,
8493 authority_indexed_plane: 0,
8494 staged: self.staged_formula_index.revision(),
8495 symbols: self.graph.symbol_revision(),
8496 semantic: crate::function_registry::semantic_epoch(),
8497 provider: self.resolver.planning_semantic_revision(),
8498 }
8499 }
8500
8501 fn planning_revision_snapshot(&self) -> PlanningRevisionSnapshot {
8502 let registry_guard = crate::function_registry::semantic_epoch_read_guard();
8503 let provider = self.resolver.planning_semantic_revision();
8504 let semantic = registry_guard.epoch();
8505 PlanningRevisionSnapshot {
8506 engine_topology_epoch: self.topology_epoch,
8507 graph_topology_revision: self.graph.topology_revision(),
8508 staged: self.staged_formula_index.revision(),
8509 symbols: self.graph.symbol_revision(),
8510 semantic,
8511 provider,
8512 deterministic_mode: self.config.deterministic_mode.clone(),
8513 budgets: self.evaluation_resource_budgets.clone(),
8514 }
8515 }
8516
8517 fn recalc_plan_key(&self) -> RecalcPlanKey {
8518 RecalcPlanKey {
8519 engine_token: Arc::clone(&self.recalc_plan_token),
8520 revisions: self.planning_revision_snapshot(),
8521 }
8522 }
8523
8524 fn plan_stale(reason: formualizer_common::PlanStaleReason) -> ExcelError {
8525 ExcelError::new(ExcelErrorKind::Value)
8526 .with_message(format!("recalculation plan is stale: {}", reason.as_str()))
8527 .with_extra(formualizer_common::ExcelErrorExtra::PlanStale { reason })
8528 }
8529
8530 fn validate_recalc_plan_key(&self, key: &RecalcPlanKey) -> Result<(), ExcelError> {
8531 use formualizer_common::PlanStaleReason;
8532
8533 if !Arc::ptr_eq(&key.engine_token, &self.recalc_plan_token) {
8534 return Err(Self::plan_stale(PlanStaleReason::Engine));
8535 }
8536
8537 let current = self.planning_revision_snapshot();
8538 let expected = &key.revisions;
8539 let stale = if expected.provider != current.provider {
8540 Some(PlanStaleReason::Provider)
8541 } else if expected.semantic != current.semantic {
8542 Some(PlanStaleReason::Semantic)
8543 } else if expected.budgets != current.budgets
8544 || expected.deterministic_mode != current.deterministic_mode
8545 {
8546 Some(PlanStaleReason::Budget)
8547 } else if expected.staged != current.staged {
8548 Some(PlanStaleReason::Staged)
8549 } else if expected.symbols != current.symbols {
8550 Some(PlanStaleReason::Symbols)
8551 } else if expected.graph_topology_revision != current.graph_topology_revision
8552 || expected.engine_topology_epoch != current.engine_topology_epoch
8553 {
8554 Some(PlanStaleReason::Graph)
8555 } else {
8556 None
8557 };
8558 stale.map_or(Ok(()), |reason| Err(Self::plan_stale(reason)))
8559 }
8560
8561 fn target_preparation_checkpoint(
8562 &mut self,
8563 deadline: Option<std::time::Instant>,
8564 work: u64,
8565 ) -> Result<(), ExcelError> {
8566 if self
8567 .active_cancel_flag
8568 .as_ref()
8569 .is_some_and(|cancel| cancel.is_cancelled())
8570 {
8571 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8572 .with_message("target graph preparation cancelled"));
8573 }
8574 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
8575 return Err(crate::engine::ResourceLedgerError::Exhausted(
8576 formualizer_common::ResourceExhaustionDetail {
8577 reason: formualizer_common::ResourceExhaustionReason::Deadline,
8578 limit: 0,
8579 observed: 1,
8580 request_id: self
8581 .active_evaluation_resource_request
8582 .as_ref()
8583 .map(|stats| stats.request_id),
8584 },
8585 )
8586 .into_excel_error());
8587 }
8588 self.charge_bounded_work(work)
8589 }
8590
8591 fn opaque_reason_in_ast(
8592 &self,
8593 ast: &ASTNode,
8594 provider: &crate::function_registry::RegistryPlanningSnapshot,
8595 ) -> Option<crate::engine::OpaqueReason> {
8596 match &ast.node_type {
8597 ASTNodeType::Function { name, args } => {
8598 let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
8599 if canonical == "INDIRECT" {
8600 return Some(crate::engine::OpaqueReason::RuntimeTextReference);
8601 }
8602 let Some(caps) = provider.function_capabilities("", name) else {
8603 return Some(crate::engine::OpaqueReason::UnknownFunction);
8604 };
8605 if caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
8606 || caps.contains(FnCaps::RETURNS_REFERENCE)
8607 {
8608 return Some(crate::engine::OpaqueReason::DynamicReference);
8609 }
8610 args.iter()
8611 .find_map(|arg| self.opaque_reason_in_ast(arg, provider))
8612 }
8613 ASTNodeType::Call { .. } => Some(crate::engine::OpaqueReason::UnknownCustomFunction),
8614 ASTNodeType::UnaryOp { expr, .. } => self.opaque_reason_in_ast(expr, provider),
8615 ASTNodeType::BinaryOp { left, right, .. } => self
8616 .opaque_reason_in_ast(left, provider)
8617 .or_else(|| self.opaque_reason_in_ast(right, provider)),
8618 ASTNodeType::Array(rows) => rows
8619 .iter()
8620 .flat_map(|row| row.iter())
8621 .find_map(|item| self.opaque_reason_in_ast(item, provider)),
8622 ASTNodeType::Reference {
8623 reference:
8624 ReferenceType::Cell {
8625 sheet: Some(sheet), ..
8626 }
8627 | ReferenceType::Range {
8628 sheet: Some(sheet), ..
8629 },
8630 ..
8631 } if self.graph.sheet_id(sheet).is_none() => {
8632 Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding)
8633 }
8634 ASTNodeType::Reference {
8635 reference:
8636 ReferenceType::External(_)
8637 | ReferenceType::Cell3D { .. }
8638 | ReferenceType::Range3D { .. },
8639 ..
8640 } => Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
8641 ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => None,
8642 }
8643 }
8644
8645 fn target_planning_snapshot(
8646 &mut self,
8647 ast: &ASTNode,
8648 planning_requests: &mut BTreeSet<(String, String, usize)>,
8649 ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
8650 #[cfg(test)]
8651 if std::mem::take(&mut self.inject_target_semantic_stale_once_for_test) {
8652 return Err(Self::preparation_stale(
8653 formualizer_common::PreparationStaleReason::Semantic,
8654 "injected target semantic preparation movement",
8655 ));
8656 }
8657 #[cfg(test)]
8658 if let Some(hook) = self.before_target_planning_snapshot_hook.take() {
8659 hook();
8660 }
8661 let mut requests = Vec::new();
8662 Self::collect_planning_function_requests(ast, &mut requests);
8663 requests.sort();
8664 requests.dedup();
8665 planning_requests.extend(requests.iter().cloned());
8666 crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
8667 &self.resolver,
8668 requests,
8669 )
8670 .map_err(|error| {
8671 ExcelError::new(ExcelErrorKind::Value)
8672 .with_message(format!("target planning snapshot unavailable: {error:?}"))
8673 })
8674 }
8675
8676 fn target_planning_snapshot_stale_reason(
8677 snapshot: &crate::function_registry::RegistryPlanningSnapshot,
8678 assumptions: &crate::engine::PreparationRevision,
8679 ) -> Option<formualizer_common::PreparationStaleReason> {
8680 if snapshot
8681 .provider_revision()
8682 .is_some_and(|revision| Some(revision) != assumptions.provider)
8683 {
8684 Some(formualizer_common::PreparationStaleReason::Provider)
8685 } else if snapshot.epoch() != assumptions.semantic
8686 && snapshot.semantic_changes_affect_requests_since(assumptions.semantic)
8687 {
8688 Some(formualizer_common::PreparationStaleReason::Semantic)
8689 } else {
8690 None
8691 }
8692 }
8693
8694 fn widen_target_preparation(
8695 policy: crate::engine::OpaquePreparePolicy,
8696 scope: &mut crate::engine::PrepareScope,
8697 reasons: &mut Vec<crate::engine::OpaqueReason>,
8698 reason: crate::engine::OpaqueReason,
8699 ) -> Result<bool, ExcelError> {
8700 if policy == crate::engine::OpaquePreparePolicy::Error {
8701 return Err(ExcelError::new(ExcelErrorKind::NImpl)
8702 .with_message(format!("opaque target preparation semantics: {reason:?}")));
8703 }
8704 if !reasons.contains(&reason) {
8705 reasons.push(reason);
8706 }
8707 if !matches!(scope, crate::engine::PrepareScope::Workbook) {
8708 *scope = crate::engine::PrepareScope::Workbook;
8709 Ok(true)
8710 } else {
8711 Ok(false)
8712 }
8713 }
8714
8715 fn widen_target_preparation_to_sheet(
8716 policy: crate::engine::OpaquePreparePolicy,
8717 scope: &mut crate::engine::PrepareScope,
8718 reasons: &mut Vec<crate::engine::OpaqueReason>,
8719 reason: crate::engine::OpaqueReason,
8720 sheet: &str,
8721 ) -> Result<bool, ExcelError> {
8722 if policy == crate::engine::OpaquePreparePolicy::Error {
8723 return Err(ExcelError::new(ExcelErrorKind::NImpl)
8724 .with_message(format!("opaque target preparation semantics: {reason:?}")));
8725 }
8726 if !reasons.contains(&reason) {
8727 reasons.push(reason);
8728 }
8729 match scope {
8730 crate::engine::PrepareScope::Exact => {
8731 *scope = crate::engine::PrepareScope::Sheets(vec![sheet.to_string()]);
8732 Ok(true)
8733 }
8734 crate::engine::PrepareScope::Sheets(sheets) => {
8735 if sheets.iter().any(|candidate| candidate == sheet) {
8736 Ok(false)
8737 } else {
8738 sheets.push(sheet.to_string());
8739 sheets.sort();
8740 Ok(true)
8741 }
8742 }
8743 crate::engine::PrepareScope::Workbook => Ok(false),
8744 }
8745 }
8746
8747 fn ast_has_proven_sheet_local_dynamic(
8748 ast: &ASTNode,
8749 provider: &crate::function_registry::RegistryPlanningSnapshot,
8750 ) -> bool {
8751 fn classify(
8752 ast: &ASTNode,
8753 provider: &crate::function_registry::RegistryPlanningSnapshot,
8754 ) -> (bool, bool) {
8755 match &ast.node_type {
8756 ASTNodeType::Function { name, args } => {
8757 let Some(caps) = provider.function_capabilities("", name) else {
8758 return (false, false);
8759 };
8760 let dynamic = caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
8761 || caps.contains(FnCaps::RETURNS_REFERENCE);
8762 let canonical = name.rsplit('.').next().unwrap_or(name);
8763 if dynamic
8764 && !canonical.eq_ignore_ascii_case("OFFSET")
8765 && !canonical.eq_ignore_ascii_case("INDEX")
8766 {
8767 return (false, true);
8768 }
8769 let mut has_dynamic = dynamic;
8770 for arg in args {
8771 let (safe, child_dynamic) = classify(arg, provider);
8772 if !safe {
8773 return (false, has_dynamic || child_dynamic);
8774 }
8775 has_dynamic |= child_dynamic;
8776 }
8777 (true, has_dynamic)
8778 }
8779 ASTNodeType::UnaryOp { expr, .. } => classify(expr, provider),
8780 ASTNodeType::BinaryOp { left, right, .. } => {
8781 let (left_safe, left_dynamic) = classify(left, provider);
8782 let (right_safe, right_dynamic) = classify(right, provider);
8783 (left_safe && right_safe, left_dynamic || right_dynamic)
8784 }
8785 ASTNodeType::Array(rows) => {
8786 let mut has_dynamic = false;
8787 for item in rows.iter().flatten() {
8788 let (safe, child_dynamic) = classify(item, provider);
8789 if !safe {
8790 return (false, has_dynamic || child_dynamic);
8791 }
8792 has_dynamic |= child_dynamic;
8793 }
8794 (true, has_dynamic)
8795 }
8796 ASTNodeType::Reference {
8797 reference:
8798 ReferenceType::Cell { sheet: None, .. }
8799 | ReferenceType::Range { sheet: None, .. },
8800 ..
8801 }
8802 | ASTNodeType::Literal(_)
8803 | ASTNodeType::Omitted => (true, false),
8804 ASTNodeType::Call { .. } | ASTNodeType::Reference { .. } => (false, false),
8805 }
8806 }
8807
8808 let (safe, dynamic) = classify(ast, provider);
8809 safe && dynamic
8810 }
8811
8812 fn table_selection_region(
8813 &self,
8814 entry: &crate::engine::graph::TableEntry,
8815 selection: &crate::engine::TableSelection,
8816 ) -> Result<PreparationRegion, ExcelError> {
8817 let mut start_row = entry.range.start.coord.row() + 1;
8818 let mut end_row = entry.range.end.coord.row() + 1;
8819 let mut start_col = entry.range.start.coord.col() + 1;
8820 let mut end_col = entry.range.end.coord.col() + 1;
8821 match selection {
8822 crate::engine::TableSelection::Whole => {}
8823 crate::engine::TableSelection::Headers => {
8824 if !entry.header_row {
8825 return Err(ExcelError::new(ExcelErrorKind::Value)
8826 .with_message(format!("table {} has no header row", entry.name)));
8827 }
8828 end_row = start_row;
8829 }
8830 crate::engine::TableSelection::Data => {
8831 if entry.header_row {
8832 start_row = start_row.saturating_add(1);
8833 }
8834 if entry.totals_row {
8835 end_row = end_row.saturating_sub(1);
8836 }
8837 }
8838 crate::engine::TableSelection::Totals => {
8839 if !entry.totals_row {
8840 return Err(ExcelError::new(ExcelErrorKind::Value)
8841 .with_message(format!("table {} has no totals row", entry.name)));
8842 }
8843 start_row = end_row;
8844 }
8845 crate::engine::TableSelection::Column(column) => {
8846 let index = entry.col_index(column).ok_or_else(|| {
8847 ExcelError::new(ExcelErrorKind::Name)
8848 .with_message(format!("unknown table column: {column}"))
8849 })?;
8850 start_col = start_col.saturating_add(index as u32);
8851 end_col = start_col;
8852 }
8853 crate::engine::TableSelection::Columns { start, end } => {
8854 let first = entry.col_index(start).ok_or_else(|| {
8855 ExcelError::new(ExcelErrorKind::Name)
8856 .with_message(format!("unknown table column: {start}"))
8857 })?;
8858 let last = entry.col_index(end).ok_or_else(|| {
8859 ExcelError::new(ExcelErrorKind::Name)
8860 .with_message(format!("unknown table column: {end}"))
8861 })?;
8862 if first > last {
8863 return Err(ExcelError::new(ExcelErrorKind::Value)
8864 .with_message("table column selection is reversed"));
8865 }
8866 start_col = start_col.saturating_add(first as u32);
8867 end_col = entry
8868 .range
8869 .start
8870 .coord
8871 .col()
8872 .saturating_add(last as u32)
8873 .saturating_add(1);
8874 }
8875 }
8876 if start_row > end_row && matches!(selection, crate::engine::TableSelection::Data) {
8877 start_row = entry.range.start.coord.row() + 1;
8878 end_row = start_row;
8879 }
8880 if start_row > end_row || start_col > end_col {
8881 return Err(
8882 ExcelError::new(ExcelErrorKind::Value).with_message("table selection is empty")
8883 );
8884 }
8885 Ok(PreparationRegion {
8886 sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
8887 sheet_id: entry.sheet_id(),
8888 start_row,
8889 start_col,
8890 end_row,
8891 end_col,
8892 })
8893 }
8894
8895 #[cfg(test)]
8896 pub(crate) fn reset_target_root_dedup_probes_for_test() {
8897 TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(0));
8898 }
8899
8900 #[cfg(test)]
8901 pub(crate) fn target_root_dedup_probes_for_test() -> usize {
8902 TARGET_ROOT_DEDUP_PROBES.with(std::cell::Cell::get)
8903 }
8904
8905 pub(crate) fn resolve_target_producers(
8906 &mut self,
8907 targets: &[crate::engine::EvaluationTarget],
8908 ) -> Result<Vec<crate::engine::target_preparation::TargetProducer>, ExcelError> {
8909 use crate::engine::target_preparation::TargetProducer;
8910
8911 let request_id = self
8912 .active_evaluation_resource_request
8913 .as_ref()
8914 .map(|request| request.request_id);
8915 let mut roots = OrderedTargetProducers::with_capacity(targets.len())
8916 .map_err(|_| target_root_allocation_error(targets.len(), request_id))?;
8917 let resolve_region = |engine: &mut Self,
8918 region: Region,
8919 value_only: Option<CellRef>,
8920 roots: &mut OrderedTargetProducers|
8921 -> Result<(), ExcelError> {
8922 let before = roots.len();
8923 for anchor in engine.graph.spill_anchors_in_region(
8924 region.sheet_id(),
8925 region.axis_ranges().0.query_bounds().0,
8926 region.axis_ranges().1.query_bounds().0,
8927 region.axis_ranges().0.query_bounds().1,
8928 region.axis_ranges().1.query_bounds().1,
8929 ) {
8930 roots
8931 .push(TargetProducer::Legacy(anchor))
8932 .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8933 }
8934 for vertex in engine.graph.vertices_in_region(
8935 region.sheet_id(),
8936 region.axis_ranges().0.query_bounds().0,
8937 region.axis_ranges().0.query_bounds().1,
8938 region.axis_ranges().1.query_bounds().0,
8939 region.axis_ranges().1.query_bounds().1,
8940 ) {
8941 let vertex = engine
8942 .graph
8943 .get_cell_ref(vertex)
8944 .and_then(|cell| engine.graph.spill_registry_anchor_for_cell(cell))
8945 .unwrap_or(vertex);
8946 if matches!(
8951 engine.graph.get_vertex_kind(vertex),
8952 VertexKind::FormulaScalar | VertexKind::FormulaArray
8953 ) {
8954 roots
8955 .push(TargetProducer::Legacy(vertex))
8956 .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8957 }
8958 }
8959 if roots.len() == before
8960 && let Some(cell) = value_only
8961 {
8962 roots
8963 .push(TargetProducer::ValueOnly(cell))
8964 .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8965 }
8966 Ok(())
8967 };
8968
8969 for target in targets {
8970 match target {
8971 crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
8972 if *row == 0 || *col == 0 {
8973 return Err(ExcelError::new(ExcelErrorKind::Ref)
8974 .with_message("target cell coordinates are one-based"));
8975 }
8976 let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
8977 ExcelError::new(ExcelErrorKind::Ref)
8978 .with_message(format!("target sheet not found: {sheet}"))
8979 })?;
8980 let cell = CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true));
8981 resolve_region(
8982 self,
8983 Region::point(sheet_id, *row - 1, *col - 1),
8984 Some(cell),
8985 &mut roots,
8986 )?;
8987 }
8988 crate::engine::EvaluationTarget::Range(range) => {
8989 let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8990 ExcelError::new(ExcelErrorKind::Ref)
8991 .with_message(format!("target sheet not found: {}", range.sheet))
8992 })?;
8993 resolve_region(
8994 self,
8995 Region::rect(
8996 sheet_id,
8997 range.start_row - 1,
8998 range.end_row - 1,
8999 range.start_col - 1,
9000 range.end_col - 1,
9001 ),
9002 None,
9003 &mut roots,
9004 )?;
9005 }
9006 crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
9007 let scope = self.name_query_scope(scope_sheet.as_deref())?;
9008 if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, scope) {
9009 roots
9010 .push(TargetProducer::Symbol(entry.vertex))
9011 .map_err(|_| {
9012 target_root_allocation_error(roots.len() + 1, request_id)
9013 })?;
9014 }
9015 }
9016 crate::engine::EvaluationTarget::Table { name, .. } => {
9017 if let Some(entry) = self.graph.resolve_table_entry(name) {
9018 roots
9019 .push(TargetProducer::Symbol(entry.vertex))
9020 .map_err(|_| {
9021 target_root_allocation_error(roots.len() + 1, request_id)
9022 })?;
9023 }
9024 }
9025 }
9026 }
9027 Ok(roots.into_vec())
9028 }
9029
9030 pub fn prepare_graph_for_targets(
9034 &mut self,
9035 targets: &[crate::engine::EvaluationTarget],
9036 options: crate::engine::TargetEvalOptions<'_>,
9037 ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9038 self.ensure_not_poisoned()?;
9039 let previous_budgets = self.evaluation_resource_budgets.clone();
9040 let diagnostics_len = self.formula_parse_diagnostics.len();
9041 let previous_report = self.last_formula_ingest_report.clone();
9042 if let Some(budgets) = options.budgets {
9043 self.evaluation_resource_budgets = budgets.clone();
9044 }
9045 let previous_graph_budget_override = self
9046 .graph
9047 .set_admission_budget_override(Some(self.evaluation_resource_budgets.clone()));
9048 let previous_cancel = self.active_cancel_flag.take();
9052 self.active_cancel_flag = options.cancel.clone();
9053 let result = self.observe_evaluation_resource_request(
9054 EvaluationRequestKind::TargetPreparation,
9055 |engine| engine.prepare_graph_for_targets_unobserved(targets, &options),
9056 );
9057 self.active_cancel_flag = previous_cancel;
9058 self.graph
9059 .set_admission_budget_override(previous_graph_budget_override);
9060 self.evaluation_resource_budgets = previous_budgets;
9061 if result.is_err() {
9062 self.formula_parse_diagnostics.truncate(diagnostics_len);
9063 self.last_formula_ingest_report = previous_report;
9064 }
9065 result
9066 }
9067
9068 fn prepare_graph_for_targets_unobserved(
9069 &mut self,
9070 targets: &[crate::engine::EvaluationTarget],
9071 options: &crate::engine::TargetEvalOptions<'_>,
9072 ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9073 let scratch_checkpoint = self
9074 .active_resource_ledger
9075 .as_ref()
9076 .map_or(0, crate::engine::ResourceLedger::scratch_checkpoint);
9077 let result = self.prepare_graph_for_targets_transaction(targets, options);
9078 let release = self
9079 .active_resource_ledger
9080 .as_mut()
9081 .map_or(Ok(()), |ledger| {
9082 ledger.release_scratch_to(scratch_checkpoint)
9083 });
9084 match (result, release) {
9085 (result, Ok(())) => result,
9086 (Ok(_), Err(error)) | (Err(_), Err(error)) => Err(error.into_excel_error()),
9087 }
9088 }
9089
9090 fn prepare_graph_for_targets_transaction(
9091 &mut self,
9092 targets: &[crate::engine::EvaluationTarget],
9093 options: &crate::engine::TargetEvalOptions<'_>,
9094 ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9095 use crate::engine::{
9096 OpaqueReason, PreparationOutcome, PrepareScope, PreparedTargetGraphReport,
9097 TableSelection,
9098 };
9099
9100 self.target_preparation_checkpoint(options.deadline, 0)?;
9101 self.observe_function_semantic_epoch()?;
9102 let assumptions = self.preparation_revisions();
9103 let ledger_at_start = self
9104 .active_resource_ledger
9105 .as_ref()
9106 .map(|ledger| ledger.snapshot());
9107 let diagnostics_len = self.formula_parse_diagnostics.len();
9108 let report_len = self.last_formula_ingest_report.clone();
9109 let request_id = options.request_id.or_else(|| {
9110 self.active_evaluation_resource_request
9111 .as_ref()
9112 .map(|stats| stats.request_id)
9113 });
9114
9115 let mut scope = PrepareScope::Exact;
9116 let mut reasons = Vec::new();
9117 let mut regions = VecDeque::new();
9118 let mut deferred_shared_regions = VecDeque::new();
9119 let mut normalized = Vec::with_capacity(targets.len());
9120 let mut symbol_vertices = VecDeque::new();
9121 for target in targets {
9122 self.target_preparation_checkpoint(options.deadline, 1)?;
9123 match target {
9124 crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
9125 if *row == 0 || *col == 0 {
9126 return Err(ExcelError::new(ExcelErrorKind::Ref)
9127 .with_message("target cell coordinates are one-based"));
9128 }
9129 let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
9130 ExcelError::new(ExcelErrorKind::Ref)
9131 .with_message(format!("target sheet not found: {sheet}"))
9132 })?;
9133 regions.push_back(PreparationRegion {
9134 sheet: sheet.clone(),
9135 sheet_id,
9136 start_row: *row,
9137 start_col: *col,
9138 end_row: *row,
9139 end_col: *col,
9140 });
9141 normalized.push(target.clone());
9142 }
9143 crate::engine::EvaluationTarget::Range(range) => {
9144 if range.start_row == 0
9145 || range.start_col == 0
9146 || range.end_row < range.start_row
9147 || range.end_col < range.start_col
9148 {
9149 return Err(ExcelError::new(ExcelErrorKind::Ref)
9150 .with_message("invalid target range"));
9151 }
9152 let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
9153 ExcelError::new(ExcelErrorKind::Ref)
9154 .with_message(format!("target sheet not found: {}", range.sheet))
9155 })?;
9156 regions.push_back(PreparationRegion {
9157 sheet: range.sheet.clone(),
9158 sheet_id,
9159 start_row: range.start_row,
9160 start_col: range.start_col,
9161 end_row: range.end_row,
9162 end_col: range.end_col,
9163 });
9164 normalized.push(target.clone());
9165 }
9166 crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
9167 let name_scope = self.name_query_scope(scope_sheet.as_deref())?;
9168 if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, name_scope) {
9169 symbol_vertices.push_back(entry.vertex);
9170 } else {
9171 Self::widen_target_preparation(
9172 options.opaque_policy,
9173 &mut scope,
9174 &mut reasons,
9175 OpaqueReason::UnresolvedName,
9176 )?;
9177 }
9178 normalized.push(target.clone());
9179 }
9180 crate::engine::EvaluationTarget::Table { name, selection } => {
9181 if let Some(entry) = self.graph.resolve_table_entry(name) {
9182 let region = self.table_selection_region(entry, selection)?;
9183 symbol_vertices.push_back(entry.vertex);
9184 regions.push_back(region);
9185 } else {
9186 Self::widen_target_preparation(
9187 options.opaque_policy,
9188 &mut scope,
9189 &mut reasons,
9190 OpaqueReason::UnresolvedTable,
9191 )?;
9192 }
9193 normalized.push(target.clone());
9194 }
9195 }
9196 }
9197
9198 let mut visited_regions = FxHashSet::default();
9199 let mut visited_vertices = FxHashSet::default();
9200 let mut selected = FxHashSet::default();
9201 let mut prepared = Vec::new();
9202 let mut pending_diagnostics = Vec::new();
9203 let mut planning_requests = BTreeSet::new();
9204 let mut selected_cells = Vec::new();
9205 let mut workbook_seeded = false;
9206 let mut sheet_scope_seeded = BTreeSet::new();
9207 let mut indexed_query_sheets = FxHashSet::default();
9208 let mut discovery_scratch_reserved = 0u64;
9209 let mut package_encountered = false;
9210 let mut selected_package_sheets = FxHashSet::default();
9211 let mut selected_package_points: BTreeMap<String, BTreeSet<(u32, u32)>> = BTreeMap::new();
9212 let mut prepared_packages: Vec<PreparedTargetSourcePackage> = Vec::new();
9213 let authoritative_with_ordinary = false;
9214 let has_unknown_package_sheet = self
9215 .staged_formula_index
9216 .package_sheets()
9217 .any(|sheet| self.graph.sheet_id(sheet).is_none());
9218
9219 loop {
9220 if let PrepareScope::Sheets(sheets) = &scope {
9221 for sheet in sheets.clone() {
9222 if !sheet_scope_seeded.insert(sheet.clone()) {
9223 continue;
9224 }
9225 let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9226 package_encountered = true;
9227 Self::widen_target_preparation(
9228 options.opaque_policy,
9229 &mut scope,
9230 &mut reasons,
9231 OpaqueReason::UnsupportedSourceSemantics,
9232 )?;
9233 continue;
9234 };
9235 for lease in self.staged_formula_index.leases_for_sheet(&sheet) {
9236 self.target_preparation_checkpoint(options.deadline, 1)?;
9237 regions.push_back(PreparationRegion {
9238 sheet: sheet.clone(),
9239 sheet_id,
9240 start_row: lease.row,
9241 start_col: lease.col,
9242 end_row: lease.row,
9243 end_col: lease.col,
9244 });
9245 }
9246 if self
9247 .staged_formula_index
9248 .package_lease_for_sheet(&sheet)
9249 .is_some()
9250 {
9251 regions.push_back(PreparationRegion {
9252 sheet: sheet.clone(),
9253 sheet_id,
9254 start_row: 1,
9255 start_col: 1,
9256 end_row: self.workbook_load_limits.max_sheet_rows,
9257 end_col: self.workbook_load_limits.max_sheet_cols,
9258 });
9259 }
9260 }
9261 }
9262 if matches!(scope, PrepareScope::Workbook) && !workbook_seeded {
9263 workbook_seeded = true;
9264 for (sheet, lease) in self.staged_formula_index.all_leases() {
9265 self.target_preparation_checkpoint(options.deadline, 1)?;
9266 let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9267 package_encountered = true;
9268 Self::widen_target_preparation(
9269 options.opaque_policy,
9270 &mut scope,
9271 &mut reasons,
9272 OpaqueReason::UnsupportedSourceSemantics,
9273 )?;
9274 continue;
9275 };
9276 regions.push_back(PreparationRegion {
9277 sheet,
9278 sheet_id,
9279 start_row: lease.row,
9280 start_col: lease.col,
9281 end_row: lease.row,
9282 end_col: lease.col,
9283 });
9284 }
9285 let package_sheets = self
9286 .staged_formula_index
9287 .package_sheets()
9288 .map(str::to_string)
9289 .collect::<Vec<_>>();
9290 for sheet in package_sheets {
9291 let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9292 package_encountered = true;
9293 Self::widen_target_preparation(
9294 options.opaque_policy,
9295 &mut scope,
9296 &mut reasons,
9297 OpaqueReason::UnsupportedSourceSemantics,
9298 )?;
9299 continue;
9300 };
9301 regions.push_back(PreparationRegion {
9302 sheet,
9303 sheet_id,
9304 start_row: 1,
9305 start_col: 1,
9306 end_row: self.workbook_load_limits.max_sheet_rows,
9307 end_col: self.workbook_load_limits.max_sheet_cols,
9308 });
9309 }
9310 }
9311
9312 let allow_partial_shared = regions.is_empty() && symbol_vertices.is_empty();
9315 let next_region = if allow_partial_shared {
9316 deferred_shared_regions.pop_front()
9317 } else {
9318 regions.pop_front()
9319 };
9320 let Some(region) = next_region else {
9321 if let Some(vertex) = symbol_vertices.pop_front() {
9322 self.target_preparation_checkpoint(options.deadline, 1)?;
9323 if !visited_vertices.insert(vertex) || !self.graph.vertex_exists(vertex) {
9324 continue;
9325 }
9326 let vertex_is_dynamic = self.graph.is_dynamic(vertex);
9327 if let Some(ast) = self.graph.get_formula(vertex) {
9328 let snapshot =
9329 self.target_planning_snapshot(&ast, &mut planning_requests)?;
9330 if let Some(reason) =
9331 Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9332 {
9333 return Err(Self::preparation_stale(
9334 reason,
9335 "target planning snapshot became stale during discovery",
9336 ));
9337 }
9338 let opaque = self.opaque_reason_in_ast(&ast, &snapshot);
9339 if let Some(reason) =
9340 opaque.or(vertex_is_dynamic.then_some(OpaqueReason::DynamicReference))
9341 {
9342 if reason == OpaqueReason::DynamicReference
9343 && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
9344 {
9345 let sheet = self.graph.get_vertex_sheet_id(vertex);
9346 let sheet = self.graph.sheet_name(sheet).to_string();
9347 Self::widen_target_preparation_to_sheet(
9348 options.opaque_policy,
9349 &mut scope,
9350 &mut reasons,
9351 reason,
9352 &sheet,
9353 )?;
9354 } else {
9355 Self::widen_target_preparation(
9356 options.opaque_policy,
9357 &mut scope,
9358 &mut reasons,
9359 reason,
9360 )?;
9361 }
9362 }
9363 } else if vertex_is_dynamic {
9364 Self::widen_target_preparation(
9365 options.opaque_policy,
9366 &mut scope,
9367 &mut reasons,
9368 OpaqueReason::DynamicReference,
9369 )?;
9370 }
9371 if let Some(anchor) = self
9372 .graph
9373 .get_cell_ref(vertex)
9374 .and_then(|cell| self.graph.spill_registry_anchor_for_cell(cell))
9375 {
9376 symbol_vertices.push_back(anchor);
9377 }
9378 if let Some(cell) = self.graph.get_cell_ref(vertex) {
9379 let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
9380 regions.push_back(PreparationRegion {
9381 sheet,
9382 sheet_id: cell.sheet_id,
9383 start_row: cell.coord.row() + 1,
9384 start_col: cell.coord.col() + 1,
9385 end_row: cell.coord.row() + 1,
9386 end_col: cell.coord.col() + 1,
9387 });
9388 }
9389 match self.graph.authority_vertex_precedents(vertex) {
9393 Some(precedents) => {
9394 for (sheet_id, rect) in precedents {
9395 self.target_preparation_checkpoint(options.deadline, 1)?;
9396 if sheet_id == crate::engine::authority::geom::SYMBOL_SHEET {
9397 for slot in rect.r0..=rect.r1 {
9398 if let Some(symbol) =
9399 self.graph.authority_host().symbols().vertex(slot)
9400 {
9401 symbol_vertices.push_back(symbol);
9402 }
9403 }
9404 continue;
9405 }
9406 let sheet = self.graph.sheet_name(sheet_id).to_string();
9407 regions.push_back(PreparationRegion {
9408 sheet,
9409 sheet_id,
9410 start_row: rect.r0 + 1,
9411 start_col: rect.c0 + 1,
9412 end_row: (rect.r1 + 1)
9413 .min(self.workbook_load_limits.max_sheet_rows),
9414 end_col: (rect.c1 + 1)
9415 .min(self.workbook_load_limits.max_sheet_cols),
9416 });
9417 }
9418 }
9419 None => {
9421 if Self::widen_target_preparation(
9422 options.opaque_policy,
9423 &mut scope,
9424 &mut reasons,
9425 OpaqueReason::UnresolvedCrossSheetBinding,
9426 )? {
9427 continue;
9428 }
9429 }
9430 }
9431 if let Some(name) = self.graph.named_range_by_vertex(vertex).cloned() {
9432 match &name.definition {
9433 NamedDefinition::Cell(cell) => regions.push_back(PreparationRegion {
9434 sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
9435 sheet_id: cell.sheet_id,
9436 start_row: cell.coord.row() + 1,
9437 start_col: cell.coord.col() + 1,
9438 end_row: cell.coord.row() + 1,
9439 end_col: cell.coord.col() + 1,
9440 }),
9441 NamedDefinition::Range(range) => regions.push_back(PreparationRegion {
9442 sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
9443 sheet_id: range.start.sheet_id,
9444 start_row: range.start.coord.row() + 1,
9445 start_col: range.start.coord.col() + 1,
9446 end_row: range.end.coord.row() + 1,
9447 end_col: range.end.coord.col() + 1,
9448 }),
9449 NamedDefinition::Formula {
9450 ast,
9451 dependencies,
9452 range_deps,
9453 } => {
9454 let snapshot =
9455 self.target_planning_snapshot(ast, &mut planning_requests)?;
9456 if let Some(reason) = Self::target_planning_snapshot_stale_reason(
9457 &snapshot,
9458 &assumptions,
9459 ) {
9460 return Err(Self::preparation_stale(
9461 reason,
9462 "target planning snapshot became stale during discovery",
9463 ));
9464 }
9465 if let Some(reason) = self.opaque_reason_in_ast(ast, &snapshot) {
9466 Self::widen_target_preparation(
9467 options.opaque_policy,
9468 &mut scope,
9469 &mut reasons,
9470 reason,
9471 )?;
9472 }
9473 for dependency in dependencies {
9474 self.target_preparation_checkpoint(options.deadline, 1)?;
9475 symbol_vertices.push_back(*dependency);
9476 }
9477 for range in range_deps {
9478 self.target_preparation_checkpoint(options.deadline, 1)?;
9479 let context_sheet = self.graph.get_vertex_sheet_id(vertex);
9487 let Ok(sheet_id) =
9488 self.resolve_sheet_locator(&range.sheet, context_sheet)
9489 else {
9490 if Self::widen_target_preparation(
9491 options.opaque_policy,
9492 &mut scope,
9493 &mut reasons,
9494 OpaqueReason::UnresolvedCrossSheetBinding,
9495 )? {
9496 break;
9497 }
9498 continue;
9499 };
9500 regions.push_back(PreparationRegion {
9501 sheet: self.graph.sheet_name(sheet_id).to_string(),
9502 sheet_id,
9503 start_row: range
9504 .start_row
9505 .map_or(1, |bound| bound.index + 1),
9506 start_col: range
9507 .start_col
9508 .map_or(1, |bound| bound.index + 1),
9509 end_row: range.end_row.map_or(
9510 self.workbook_load_limits.max_sheet_rows,
9511 |bound| bound.index + 1,
9512 ),
9513 end_col: range.end_col.map_or(
9514 self.workbook_load_limits.max_sheet_cols,
9515 |bound| bound.index + 1,
9516 ),
9517 });
9518 }
9519 }
9520 NamedDefinition::Literal(_) => {}
9521 }
9522 }
9523 if let Some(table) = self.graph.table_by_vertex(vertex) {
9524 regions
9525 .push_back(self.table_selection_region(table, &TableSelection::Whole)?);
9526 }
9527 continue;
9528 }
9529 break;
9530 };
9531
9532 self.target_preparation_checkpoint(options.deadline, 1)?;
9533 if !visited_regions.insert(region.clone()) && !allow_partial_shared {
9534 continue;
9535 }
9536 if let PrepareScope::Sheets(sheets) = &mut scope
9537 && !sheets.iter().any(|sheet| sheet == ®ion.sheet)
9538 {
9539 sheets.push(region.sheet.clone());
9540 sheets.sort();
9541 }
9542 let package_match = self.staged_formula_index.package_for_region(
9543 ®ion.sheet,
9544 region.start_row,
9545 region.start_col,
9546 region.end_row,
9547 region.end_col,
9548 );
9549 let package_lease = match package_match {
9550 Some(Ok(lease)) => Some(lease),
9551 Some(Err(()))
9552 if region.start_row == 1
9553 && region.start_col == 1
9554 && region.end_row == self.workbook_load_limits.max_sheet_rows
9555 && region.end_col == self.workbook_load_limits.max_sheet_cols =>
9556 {
9557 self.staged_formula_index
9558 .package_lease_for_sheet(®ion.sheet)
9559 }
9560 Some(Err(())) => {
9561 Self::widen_target_preparation(
9562 options.opaque_policy,
9563 &mut scope,
9564 &mut reasons,
9565 OpaqueReason::DeferredSourcePackage,
9566 )?;
9567 None
9568 }
9569 None => None,
9570 };
9571 let compatibility_before_package_replay = package_lease.is_some()
9572 && (authoritative_with_ordinary || has_unknown_package_sheet);
9573 let package_lease = if compatibility_before_package_replay {
9574 package_encountered = true;
9575 Self::widen_target_preparation(
9576 options.opaque_policy,
9577 &mut scope,
9578 &mut reasons,
9579 OpaqueReason::UnsupportedSourceSemantics,
9580 )?;
9581 None
9582 } else {
9583 package_lease
9584 };
9585 if let Some(package_lease) = package_lease
9586 && !selected_package_sheets.contains(®ion.sheet)
9587 {
9588 self.target_preparation_checkpoint(options.deadline, 1)?;
9589 let mut points = self.staged_formula_index.package_points_in_region(
9590 ®ion.sheet,
9591 region.start_row,
9592 region.start_col,
9593 region.end_row,
9594 region.end_col,
9595 );
9596 if let Some(selected) = selected_package_points.get(®ion.sheet) {
9597 points.retain(|point| !selected.contains(point));
9598 }
9599
9600 if let Some(selected) = selected_package_points.get(®ion.sheet) {
9601 points.retain(|point| !selected.contains(point));
9602 }
9603 let permit_partial = allow_partial_shared
9604 || (points.len() == 1
9605 && regions.is_empty()
9606 && symbol_vertices.is_empty()
9607 && deferred_shared_regions.is_empty());
9608 let partial = self.prepare_target_exact_source_selection(
9609 ®ion.sheet,
9610 package_lease,
9611 points,
9612 selected_package_points
9613 .get(®ion.sheet)
9614 .unwrap_or(&BTreeSet::new()),
9615 permit_partial,
9616 options.deadline,
9617 &mut discovery_scratch_reserved,
9618 )?;
9619 let mut package = if let Some(mut package) = partial {
9620 if package.deferred_shared {
9621 deferred_shared_regions.push_back(region.clone());
9622 }
9623 if !package.direct_domains.is_empty() {
9627 for prior in prepared_packages
9628 .iter_mut()
9629 .filter(|prior| prior.sheet == region.sheet)
9630 {
9631 prior
9632 .replay_records
9633 .retain(|record| !package.direct_contains(record.row, record.col));
9634 prior
9635 .legacy
9636 .retain(|(row, col, _, _)| !package.direct_contains(*row, *col));
9637 if let Some(points) = prior.selected_points.as_mut() {
9638 points.retain(|&(row, col)| !package.direct_contains(row, col));
9639 }
9640 }
9641 }
9642 if selected_package_points.contains_key(®ion.sheet) {
9643 package.source_report = Default::default();
9644 }
9645 let points = package.selected_points.as_ref().unwrap();
9646 if !points.is_empty() {
9647 selected_package_points
9648 .entry(region.sheet.clone())
9649 .or_default()
9650 .extend(points.iter().copied());
9651 }
9652 package
9653 } else {
9654 selected_package_sheets.insert(region.sheet.clone());
9655 self.prepare_target_source_package(
9656 ®ion.sheet,
9657 package_lease,
9658 options.deadline,
9659 )?
9660 };
9661 if package
9662 .selected_points
9663 .as_ref()
9664 .is_none_or(|points| !points.is_empty())
9665 {
9666 let mut final_fallback = BTreeMap::new();
9667 for record in package.fallback_records() {
9668 final_fallback.insert((record.row, record.col), record.clone());
9669 }
9670 let batch = self.formula_batch_from_exact_replay(
9671 ®ion.sheet,
9672 final_fallback.into_values(),
9673 )?;
9674 for record in batch.formulas {
9675 self.target_preparation_checkpoint(options.deadline, 1)?;
9676 let ast = self
9677 .graph
9678 .data_store()
9679 .retrieve_ast(record.ast_id, self.graph.sheet_reg())
9680 .ok_or_else(|| {
9681 ExcelError::new(ExcelErrorKind::Value)
9682 .with_message("target fallback AST is unavailable")
9683 })?;
9684 let snapshot =
9685 self.target_planning_snapshot(&ast, &mut planning_requests)?;
9686 if let Some(reason) =
9687 Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9688 {
9689 return Err(Self::preparation_stale(
9690 reason,
9691 "target fallback planning snapshot became stale during discovery",
9692 ));
9693 }
9694 let proven_sheet_local_dynamic =
9695 Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
9696 if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
9697 if reason == OpaqueReason::DynamicReference
9698 && proven_sheet_local_dynamic
9699 {
9700 Self::widen_target_preparation_to_sheet(
9701 options.opaque_policy,
9702 &mut scope,
9703 &mut reasons,
9704 reason,
9705 ®ion.sheet,
9706 )?;
9707 } else {
9708 Self::widen_target_preparation(
9709 options.opaque_policy,
9710 &mut scope,
9711 &mut reasons,
9712 reason,
9713 )?;
9714 }
9715 }
9716 let placement = CellRef::new(
9717 package.sheet_id,
9718 Coord::from_excel(record.row, record.col, true, true),
9719 );
9720 let ingested = self
9721 .graph
9722 .ingest_pipeline(&snapshot)
9723 .enable_function_semantics()
9724 .ingest_formula(
9725 FormulaAstInput::RawArena(record.ast_id),
9726 placement,
9727 record.formula_text,
9728 )?;
9729 if ingested.dep_plan.dynamic {
9730 if proven_sheet_local_dynamic {
9731 Self::widen_target_preparation_to_sheet(
9732 options.opaque_policy,
9733 &mut scope,
9734 &mut reasons,
9735 OpaqueReason::DynamicReference,
9736 ®ion.sheet,
9737 )?;
9738 } else {
9739 Self::widen_target_preparation(
9740 options.opaque_policy,
9741 &mut scope,
9742 &mut reasons,
9743 OpaqueReason::DynamicReference,
9744 )?;
9745 }
9746 }
9747 for dep in &ingested.dep_plan.direct_cell_deps {
9748 self.target_preparation_checkpoint(options.deadline, 1)?;
9749 regions.push_back(PreparationRegion {
9750 sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
9751 sheet_id: dep.sheet_id,
9752 start_row: dep.coord.row().saturating_add(1),
9753 start_col: dep.coord.col().saturating_add(1),
9754 end_row: dep.coord.row().saturating_add(1),
9755 end_col: dep.coord.col().saturating_add(1),
9756 });
9757 }
9758 for range in &ingested.dep_plan.range_deps {
9759 self.target_preparation_checkpoint(options.deadline, 1)?;
9760 let Ok(dependency_sheet) =
9763 self.resolve_sheet_locator(&range.sheet, package.sheet_id)
9764 else {
9765 Self::widen_target_preparation(
9766 options.opaque_policy,
9767 &mut scope,
9768 &mut reasons,
9769 OpaqueReason::UnresolvedCrossSheetBinding,
9770 )?;
9771 continue;
9772 };
9773 regions.push_back(PreparationRegion {
9774 sheet: self.graph.sheet_name(dependency_sheet).to_string(),
9775 sheet_id: dependency_sheet,
9776 start_row: range.start_row.map_or(1, |bound| bound.index + 1),
9777 start_col: range.start_col.map_or(1, |bound| bound.index + 1),
9778 end_row: range
9779 .end_row
9780 .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
9781 bound.index + 1
9782 }),
9783 end_col: range
9784 .end_col
9785 .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
9786 bound.index + 1
9787 }),
9788 });
9789 }
9790 for name in ingested
9791 .dep_plan
9792 .resolved_named_refs
9793 .iter()
9794 .chain(&ingested.dep_plan.named_refs)
9795 {
9796 self.target_preparation_checkpoint(options.deadline, 1)?;
9797 if let Some(entry) =
9798 self.graph.resolve_name_entry(name, package.sheet_id)
9799 {
9800 symbol_vertices.push_back(entry.vertex);
9801 } else if self.graph.resolve_source_scalar_entry(name).is_none()
9802 && self.graph.resolve_source_table_entry(name).is_none()
9803 {
9804 Self::widen_target_preparation(
9805 options.opaque_policy,
9806 &mut scope,
9807 &mut reasons,
9808 OpaqueReason::UnresolvedName,
9809 )?;
9810 }
9811 }
9812 for table in &ingested.dep_plan.table_refs {
9813 self.target_preparation_checkpoint(options.deadline, 1)?;
9814 if let Some(entry) = self.graph.resolve_table_entry(table) {
9815 symbol_vertices.push_back(entry.vertex);
9816 } else if self.graph.resolve_source_table_entry(table).is_none() {
9817 Self::widen_target_preparation(
9818 options.opaque_policy,
9819 &mut scope,
9820 &mut reasons,
9821 OpaqueReason::UnresolvedTable,
9822 )?;
9823 }
9824 }
9825 package.legacy.push((
9826 record.row,
9827 record.col,
9828 ingested.ast_id,
9829 ingested.dep_plan,
9830 ));
9831 }
9832 prepared_packages.push(package);
9833 }
9834 }
9835 let leases = self.staged_formula_index.leases_in_region(
9836 ®ion.sheet,
9837 region.start_row,
9838 region.start_col,
9839 region.end_row,
9840 region.end_col,
9841 );
9842 for lease in leases {
9843 self.target_preparation_checkpoint(options.deadline, 1)?;
9844 let sheet_id = self.graph.sheet_id(®ion.sheet).ok_or_else(|| {
9845 ExcelError::new(ExcelErrorKind::Ref)
9846 .with_message(format!("staged formula sheet not found: {}", region.sheet))
9847 })?;
9848 let key = (region.sheet.clone(), lease.row, lease.col, lease.generation);
9849 if !selected.insert(key) {
9850 continue;
9851 }
9852 let text = self
9853 .staged_formulas
9854 .get(®ion.sheet)
9855 .and_then(|sheet| sheet.get_ordinary(lease.row, lease.col))
9856 .ok_or_else(|| {
9857 ExcelError::new(ExcelErrorKind::Value)
9858 .with_message("staged formula index is stale")
9859 })?
9860 .to_string();
9861 let formula = if text.starts_with('=') {
9862 text.clone()
9863 } else {
9864 format!("={text}")
9865 };
9866 self.target_preparation_checkpoint(options.deadline, 1)?;
9867 let ast = match formualizer_parse::parser::parse(&formula) {
9868 Ok(ast) => ast,
9869 Err(error) => {
9870 if self.config.formula_parse_policy == FormulaParsePolicy::Strict {
9871 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
9872 format!(
9873 "Formula parse error at {}!{}{}: {error}",
9874 region.sheet,
9875 col_letters_from_1based(lease.col)
9876 .unwrap_or_else(|_| "?".to_string()),
9877 lease.row
9878 ),
9879 ));
9880 }
9881 pending_diagnostics.push(FormulaParseDiagnostic {
9882 sheet: region.sheet.clone(),
9883 row: lease.row,
9884 col: lease.col,
9885 formula: formula.clone(),
9886 message: error.to_string(),
9887 policy: self.config.formula_parse_policy,
9888 });
9889 match self.config.formula_parse_policy {
9890 FormulaParsePolicy::KeepCachedValue => {
9891 selected_cells.push(
9892 formualizer_common::RangeAddress::new(
9893 region.sheet.clone(),
9894 lease.row,
9895 lease.col,
9896 lease.row,
9897 lease.col,
9898 )
9899 .expect("selected staged coordinates are valid"),
9900 );
9901 prepared.push(PreparedOrdinaryStagedFormula {
9902 sheet: region.sheet.clone(),
9903 sheet_id,
9904 lease,
9905 ast_id: None,
9906 plan: None,
9907 });
9908 continue;
9909 }
9910 FormulaParsePolicy::AsText => ASTNode::new(
9911 ASTNodeType::Literal(LiteralValue::Text(formula.clone())),
9912 None,
9913 ),
9914 FormulaParsePolicy::CoerceToError => ASTNode::new(
9915 ASTNodeType::Literal(LiteralValue::Error(
9916 ExcelError::new(ExcelErrorKind::Error)
9917 .with_message(format!("Malformed formula: {error}")),
9918 )),
9919 None,
9920 ),
9921 FormulaParsePolicy::Strict => unreachable!(),
9922 }
9923 }
9924 };
9925 self.target_preparation_checkpoint(options.deadline, 1)?;
9926 let snapshot = self.target_planning_snapshot(&ast, &mut planning_requests)?;
9927 self.target_preparation_checkpoint(options.deadline, 1)?;
9928 if let Some(reason) =
9929 Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9930 {
9931 return Err(Self::preparation_stale(
9932 reason,
9933 "target planning snapshot became stale during discovery",
9934 ));
9935 }
9936 if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
9937 if reason == OpaqueReason::DynamicReference
9938 && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
9939 {
9940 Self::widen_target_preparation_to_sheet(
9941 options.opaque_policy,
9942 &mut scope,
9943 &mut reasons,
9944 reason,
9945 ®ion.sheet,
9946 )?;
9947 } else {
9948 Self::widen_target_preparation(
9949 options.opaque_policy,
9950 &mut scope,
9951 &mut reasons,
9952 reason,
9953 )?;
9954 }
9955 }
9956 let proven_sheet_local_dynamic =
9957 Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
9958 let placement = CellRef::new(
9959 sheet_id,
9960 Coord::from_excel(lease.row, lease.col, true, true),
9961 );
9962 let ingested = self.graph.ingest_pipeline(&snapshot).ingest_formula(
9963 FormulaAstInput::Tree(ast),
9964 placement,
9965 Some(Arc::from(formula)),
9966 )?;
9967 self.target_preparation_checkpoint(options.deadline, 1)?;
9968 if ingested.dep_plan.dynamic {
9969 if proven_sheet_local_dynamic {
9970 Self::widen_target_preparation_to_sheet(
9971 options.opaque_policy,
9972 &mut scope,
9973 &mut reasons,
9974 OpaqueReason::DynamicReference,
9975 ®ion.sheet,
9976 )?;
9977 } else {
9978 Self::widen_target_preparation(
9979 options.opaque_policy,
9980 &mut scope,
9981 &mut reasons,
9982 OpaqueReason::DynamicReference,
9983 )?;
9984 }
9985 }
9986 for dep in &ingested.dep_plan.direct_cell_deps {
9987 self.target_preparation_checkpoint(options.deadline, 1)?;
9988 regions.push_back(PreparationRegion {
9989 sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
9990 sheet_id: dep.sheet_id,
9991 start_row: dep.coord.row() + 1,
9992 start_col: dep.coord.col() + 1,
9993 end_row: dep.coord.row() + 1,
9994 end_col: dep.coord.col() + 1,
9995 });
9996 }
9997 for range in &ingested.dep_plan.range_deps {
9998 self.target_preparation_checkpoint(options.deadline, 1)?;
9999 let Ok(dependency_sheet) = self.resolve_sheet_locator(&range.sheet, sheet_id)
10001 else {
10002 Self::widen_target_preparation(
10003 options.opaque_policy,
10004 &mut scope,
10005 &mut reasons,
10006 OpaqueReason::UnresolvedCrossSheetBinding,
10007 )?;
10008 continue;
10009 };
10010 regions.push_back(PreparationRegion {
10011 sheet: self.graph.sheet_name(dependency_sheet).to_string(),
10012 sheet_id: dependency_sheet,
10013 start_row: range.start_row.map_or(1, |bound| bound.index + 1),
10014 start_col: range.start_col.map_or(1, |bound| bound.index + 1),
10015 end_row: range
10016 .end_row
10017 .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
10018 bound.index + 1
10019 }),
10020 end_col: range
10021 .end_col
10022 .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
10023 bound.index + 1
10024 }),
10025 });
10026 }
10027 for name in ingested
10028 .dep_plan
10029 .resolved_named_refs
10030 .iter()
10031 .chain(&ingested.dep_plan.named_refs)
10032 {
10033 self.target_preparation_checkpoint(options.deadline, 1)?;
10034 if let Some(entry) = self.graph.resolve_name_entry(name, sheet_id) {
10035 symbol_vertices.push_back(entry.vertex);
10036 } else if self.graph.resolve_source_scalar_entry(name).is_none()
10037 && self.graph.resolve_source_table_entry(name).is_none()
10038 {
10039 Self::widen_target_preparation(
10040 options.opaque_policy,
10041 &mut scope,
10042 &mut reasons,
10043 OpaqueReason::UnresolvedName,
10044 )?;
10045 }
10046 }
10047 for table in &ingested.dep_plan.table_refs {
10048 self.target_preparation_checkpoint(options.deadline, 1)?;
10049 if let Some(entry) = self.graph.resolve_table_entry(table) {
10050 symbol_vertices.push_back(entry.vertex);
10051 } else if self.graph.resolve_source_table_entry(table).is_none() {
10052 Self::widen_target_preparation(
10053 options.opaque_policy,
10054 &mut scope,
10055 &mut reasons,
10056 OpaqueReason::UnresolvedTable,
10057 )?;
10058 }
10059 }
10060 selected_cells.push(
10061 formualizer_common::RangeAddress::new(
10062 region.sheet.clone(),
10063 lease.row,
10064 lease.col,
10065 lease.row,
10066 lease.col,
10067 )
10068 .expect("selected staged coordinates are valid"),
10069 );
10070 prepared.push(PreparedOrdinaryStagedFormula {
10071 sheet: region.sheet.clone(),
10072 sheet_id,
10073 lease,
10074 ast_id: Some(ingested.ast_id),
10075 plan: Some(ingested.dep_plan),
10076 });
10077 }
10078
10079 if indexed_query_sheets.insert(region.sheet_id) {
10080 self.graph.prepare_sheet_index_for_query(region.sheet_id);
10081 let bytes = (self.graph.sheet_index_vertex_count(region.sheet_id) as u64)
10082 .saturating_mul(32);
10083 self.reserve_graph_source_scratch(bytes)?;
10084 discovery_scratch_reserved = discovery_scratch_reserved.saturating_add(bytes);
10085 }
10086 let spill_anchors = self.graph.spill_anchors_in_region(
10087 region.sheet_id,
10088 region.start_row - 1,
10089 region.start_col - 1,
10090 region.end_row - 1,
10091 region.end_col - 1,
10092 );
10093 for anchor in spill_anchors {
10094 self.target_preparation_checkpoint(options.deadline, 1)?;
10095 symbol_vertices.push_back(anchor);
10096 }
10097 let vertices = self.graph.vertices_in_region(
10098 region.sheet_id,
10099 region.start_row - 1,
10100 region.end_row - 1,
10101 region.start_col - 1,
10102 region.end_col - 1,
10103 );
10104 for vertex in vertices {
10105 self.target_preparation_checkpoint(options.deadline, 1)?;
10106 symbol_vertices.push_back(vertex);
10107 }
10108 }
10109
10110 #[cfg(test)]
10111 self.target_preparation_fault(
10112 crate::engine::target_preparation::TargetPreparationFault::AfterDiscovery,
10113 )?;
10114
10115 if package_encountered {
10116 Self::widen_target_preparation(
10117 options.opaque_policy,
10118 &mut scope,
10119 &mut reasons,
10120 OpaqueReason::UnsupportedSourceSemantics,
10121 )?;
10122 self.target_preparation_checkpoint(options.deadline, 0)?;
10123 let selected_count = self.staged_formula_count();
10124 let selected_packages = self
10125 .staged_formulas
10126 .values()
10127 .filter_map(|staged| staged.deferred_package.as_ref())
10128 .map(|package| package.families.len() + package.partitioned_families.len())
10129 .sum();
10130 self.formula_parse_diagnostics.truncate(diagnostics_len);
10131 self.last_formula_ingest_report = report_len;
10132 #[cfg(test)]
10133 if let Some(hook) = self.before_target_preparation_commit_hook.take() {
10134 hook();
10135 }
10136 self.target_preparation_checkpoint(options.deadline, 0)?;
10137 #[cfg(test)]
10138 self.target_preparation_fault(
10139 crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
10140 )?;
10141 let current_revisions = self.preparation_revisions();
10142 if let Some(reason) = Self::preparation_revision_stale_reason(
10143 &assumptions,
10144 ¤t_revisions,
10145 &planning_requests,
10146 true,
10147 ) {
10148 return Err(Self::preparation_stale(
10149 reason,
10150 "target compatibility preparation plan is stale",
10151 ));
10152 }
10153 #[cfg(test)]
10154 self.target_preparation_fault(
10155 crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
10156 )?;
10157 let commit_work_before = self
10158 .active_resource_ledger
10159 .as_ref()
10160 .map_or(0, |ledger| ledger.snapshot().work_charged);
10161 let commit_started = crate::instant::FzInstant::now();
10162 self.build_graph_all_unobserved()?;
10163 let commit_window = commit_started.elapsed();
10164 let ledger_after = self
10165 .active_resource_ledger
10166 .as_ref()
10167 .map(|ledger| ledger.snapshot());
10168 let actual_commit_work = ledger_after
10169 .map_or(0, |snapshot| snapshot.work_charged)
10170 .saturating_sub(commit_work_before);
10171 let observed_scratch_bytes = ledger_after
10172 .map_or(0, |snapshot| snapshot.scratch_peak)
10173 .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
10174 let revisions = assumptions.clone();
10175 let report = PreparedTargetGraphReport {
10176 request_id: request_id.unwrap_or_default(),
10177 requested_targets: targets.len(),
10178 normalized_regions: visited_regions.len(),
10179 normalized_target_list: normalized,
10180 selected_staged_cells: selected_count,
10181 selected_source_families: selected_packages,
10182 retained_staged_cells: self.staged_formula_count(),
10183 selected_cells,
10184 retained_cells: Vec::new(),
10185 widened_scope: PrepareScope::Workbook,
10186 widening_reasons: reasons,
10187 revisions,
10188 commit_window,
10189 estimated_scratch_bytes: discovery_scratch_reserved
10190 .saturating_add((selected_count as u64).saturating_mul(256)),
10191 observed_scratch_bytes,
10192 estimated_commit_work: selected_count as u64,
10193 actual_commit_work,
10194 outcome: PreparationOutcome::CompatibilityPrepared,
10195 };
10196 self.observe_target_preparation_report(&report);
10197 return Ok(report);
10198 }
10199
10200 prepared.sort_by_key(|formula| formula.lease.insertion_order);
10201 for package in &prepared_packages {
10202 if let Some(points) = &package.selected_points {
10203 selected_cells.extend(points.iter().filter_map(|&(row, col)| {
10204 formualizer_common::RangeAddress::new(&package.sheet, row, col, row, col).ok()
10205 }));
10206 continue;
10207 }
10208 selected_cells.extend(package.replay_records.iter().filter_map(|record| {
10209 formualizer_common::RangeAddress::new(
10210 package.sheet.clone(),
10211 record.row,
10212 record.col,
10213 record.row,
10214 record.col,
10215 )
10216 .ok()
10217 }));
10218 }
10219 let (legacy_graph, planned_formula_count) =
10220 self.prepare_target_combined_legacy_graph(&prepared_packages, &prepared)?;
10221 let new_vertices = legacy_graph.new_vertex_count();
10222 let new_edges = legacy_graph.planned_edge_count().ok_or_else(|| {
10223 ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
10224 })?;
10225 let removed_edges = legacy_graph.removed_edge_count().ok_or_else(|| {
10226 ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
10227 })?;
10228 let current = self.graph.baseline_stats();
10229 let final_vertices = current
10230 .graph_vertex_count
10231 .checked_add(new_vertices)
10232 .ok_or_else(|| {
10233 crate::engine::ResourceLedgerError::Exhausted(
10234 formualizer_common::ResourceExhaustionDetail {
10235 reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
10236 limit: u64::MAX,
10237 observed: u64::MAX,
10238 request_id,
10239 },
10240 )
10241 .into_excel_error()
10242 })?;
10243 let final_edges = current
10244 .graph_edge_count
10245 .checked_sub(removed_edges)
10246 .and_then(|count| count.checked_add(new_edges))
10247 .ok_or_else(|| {
10248 crate::engine::ResourceLedgerError::Exhausted(
10249 formualizer_common::ResourceExhaustionDetail {
10250 reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
10251 limit: u64::MAX,
10252 observed: u64::MAX,
10253 request_id,
10254 },
10255 )
10256 .into_excel_error()
10257 })?;
10258 #[cfg(test)]
10259 self.target_preparation_fault(
10260 crate::engine::target_preparation::TargetPreparationFault::Admission,
10261 )?;
10262 let resource = |reason, limit: u64, observed: u64| {
10263 crate::engine::ResourceLedgerError::Exhausted(
10264 formualizer_common::ResourceExhaustionDetail {
10265 reason,
10266 limit,
10267 observed,
10268 request_id,
10269 },
10270 )
10271 .into_excel_error()
10272 };
10273 let admission = crate::engine::resource_ledger::GraphAdmission {
10274 final_vertices,
10275 final_edges,
10276 materialization_cells: planned_formula_count as u64,
10277 added_vertices: new_vertices,
10278 added_edges: new_edges,
10279 };
10280 let materialized_bytes = admission
10281 .materialized_graph_bytes()
10282 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
10283 if let Err(error) = self.preflight_graph_admission(admission) {
10284 if let formualizer_common::ExcelErrorExtra::Resource { detail } = error.extra() {
10285 self.observe_target_admission_failure(detail.reason);
10286 }
10287 return Err(error);
10288 }
10289 let selected_package_records = prepared_packages
10290 .iter()
10291 .map(|package| package.replay_records.len() as u64)
10292 .sum::<u64>();
10293 let planned_working_bytes = (prepared.len() as u64)
10294 .saturating_add(selected_package_records)
10295 .saturating_mul(256)
10296 .saturating_add((visited_regions.len() as u64).saturating_mul(128))
10297 .saturating_add((visited_vertices.len() as u64).saturating_mul(32))
10298 .saturating_add((selected.len() as u64).saturating_mul(96))
10299 .saturating_add(materialized_bytes);
10300 let residual_scratch = selected_package_points
10301 .iter()
10302 .map(|(sheet, points)| {
10303 let source = self
10304 .staged_formulas
10305 .get(sheet)
10306 .unwrap()
10307 .deferred_package
10308 .as_ref()
10309 .unwrap();
10310 (source.families.len() as u64)
10311 .saturating_mul(256)
10312 .saturating_add(
10313 source
10314 .partitioned_families
10315 .iter()
10316 .map(|family| {
10317 256u64
10318 .saturating_add(family.fragments.len() as u64 * 32)
10319 .saturating_add(family.legacy_members.len() as u64 * 32)
10320 })
10321 .sum::<u64>(),
10322 )
10323 .saturating_add(points.len() as u64 * 32)
10326 .saturating_add(
10327 prepared_packages
10328 .iter()
10329 .filter(|package| &package.sheet == sheet)
10330 .map(|package| package.replay_records.len() as u64 * 480)
10331 .sum::<u64>(),
10332 )
10333 })
10334 .sum::<u64>();
10335 let scratch_bytes = discovery_scratch_reserved
10336 .saturating_add(planned_working_bytes)
10337 .saturating_add(residual_scratch);
10338 let remaining_scratch = scratch_bytes.saturating_sub(discovery_scratch_reserved);
10339 if let Err(error) = self.reserve_graph_source_scratch(remaining_scratch) {
10340 self.observe_target_admission_failure(
10341 formualizer_common::ResourceExhaustionReason::ScratchMemory,
10342 );
10343 return Err(error);
10344 }
10345
10346 let mut residual_sources = BTreeMap::new();
10347 let mut residual_owners = BTreeMap::new();
10348 for (sheet, points) in &selected_package_points {
10349 let selected: BTreeMap<_, BTreeSet<_>> = prepared_packages
10350 .iter()
10351 .filter(|package| &package.sheet == sheet)
10352 .flat_map(|package| &package.replay_records)
10353 .filter_map(|record| {
10354 record.partition_owner.or(record.family).map(|owner| {
10355 (
10356 owner,
10357 crate::engine::SourceCoord {
10358 row: record.row - 1,
10359 col: record.col - 1,
10360 },
10361 )
10362 })
10363 })
10364 .fold(BTreeMap::new(), |mut map, (owner, coord)| {
10365 map.entry(owner).or_default().insert(coord);
10366 map
10367 });
10368 let metadata_work = self
10369 .staged_formulas
10370 .get(sheet)
10371 .unwrap()
10372 .deferred_package
10373 .as_ref()
10374 .map_or(0, |source| {
10375 source.families.len() as u64
10376 + source
10377 .partitioned_families
10378 .iter()
10379 .map(|family| {
10380 1 + family.fragments.len() as u64 * family.fragments.len() as u64
10381 + family.legacy_members.len() as u64
10382 })
10383 .sum::<u64>()
10384 });
10385 let split_work = selected
10386 .values()
10387 .map(|points| points.len() as u64 * 128)
10388 .sum::<u64>();
10389 self.target_preparation_checkpoint(
10390 options.deadline,
10391 metadata_work
10392 .saturating_add(split_work)
10393 .saturating_add(points.len() as u64),
10394 )?;
10395 let source = self
10396 .staged_formulas
10397 .get(sheet)
10398 .unwrap()
10399 .deferred_package
10400 .as_ref()
10401 .unwrap();
10402 let complete: BTreeSet<_> = prepared_packages
10403 .iter()
10404 .filter(|package| &package.sheet == sheet)
10405 .flat_map(|package| package.complete_selections.iter().copied())
10406 .collect();
10407 let residual = source
10408 .residual_sources(&selected, &complete, &self.workbook_load_limits)
10409 .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
10410 residual_owners.insert(
10411 sheet.clone(),
10412 residual
10413 .1
10414 .iter()
10415 .map(|family| family.source_id)
10416 .collect::<BTreeSet<_>>(),
10417 );
10418 residual_sources.insert(sheet.clone(), residual);
10419 }
10420
10421 for (sheet, points) in &selected_package_points {
10424 let package = self
10425 .staged_formulas
10426 .get_mut(sheet)
10427 .unwrap()
10428 .deferred_package
10429 .as_mut()
10430 .unwrap();
10431 let owner_count = prepared_packages
10432 .iter()
10433 .filter(|p| &p.sheet == sheet)
10434 .flat_map(|p| &p.replay_records)
10435 .filter_map(|record| record.partition_owner.or(record.family))
10436 .filter(|owner| residual_owners[sheet].contains(owner))
10437 .count();
10438 package
10439 .consumed_members
10440 .try_reserve(owner_count)
10441 .map_err(|_| {
10442 resource(
10443 formualizer_common::ResourceExhaustionReason::ScratchMemory,
10444 0,
10445 owner_count as u64 * 24,
10446 )
10447 })?;
10448 package.suppressed.try_reserve(points.len()).map_err(|_| {
10449 resource(
10450 formualizer_common::ResourceExhaustionReason::ScratchMemory,
10451 0,
10452 points.len() as u64 * 16,
10453 )
10454 })?;
10455 }
10456
10457 let estimated_commit_duration = std::time::Duration::from_nanos(
10458 (new_vertices as u64)
10459 .saturating_add(new_edges as u64)
10460 .saturating_add(prepared.len() as u64)
10461 .saturating_add(selected_package_records)
10462 .max(1)
10463 .saturating_mul(100),
10464 );
10465 if options.deadline.is_some_and(|deadline| {
10466 std::time::Instant::now()
10467 .checked_add(estimated_commit_duration)
10468 .is_none_or(|finish| finish > deadline)
10469 }) {
10470 self.observe_target_admission_failure(
10471 formualizer_common::ResourceExhaustionReason::Deadline,
10472 );
10473 return Err(resource(
10474 formualizer_common::ResourceExhaustionReason::Deadline,
10475 0,
10476 1,
10477 ));
10478 }
10479 #[cfg(test)]
10480 if let Some(hook) = self.before_target_preparation_commit_hook.take() {
10481 hook();
10482 }
10483 self.target_preparation_checkpoint(options.deadline, 0)?;
10484 #[cfg(test)]
10485 self.target_preparation_fault(
10486 crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
10487 )?;
10488 let current_revisions = self.preparation_revisions();
10489 let staged_leases_match = prepared.iter().all(|formula| {
10490 self.staged_formula_index
10491 .lease_matches(&formula.sheet, formula.lease)
10492 }) && prepared_packages.iter().all(|package| {
10493 self.staged_formula_index
10494 .package_lease_matches(&package.sheet, package.lease)
10495 });
10496 let stale_reason = Self::preparation_revision_stale_reason(
10497 &assumptions,
10498 ¤t_revisions,
10499 &planning_requests,
10500 staged_leases_match,
10501 );
10502 if let Some(reason) = stale_reason {
10503 return Err(Self::preparation_stale(
10504 reason,
10505 "target graph preparation plan is stale",
10506 ));
10507 }
10508 #[cfg(test)]
10509 self.target_preparation_fault(
10510 crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
10511 )?;
10512 self.graph
10513 .validate_prepared_legacy_graph_plan(&legacy_graph)
10514 .map_err(|error| {
10515 Self::preparation_stale(
10516 formualizer_common::PreparationStaleReason::Graph,
10517 format!("target graph preparation plan is stale: {error}"),
10518 )
10519 })?;
10520 #[cfg(test)]
10521 self.target_preparation_fault(
10522 crate::engine::target_preparation::TargetPreparationFault::Reservation,
10523 )?;
10524 self.graph.reserve_prepared_legacy_graph_plan(&legacy_graph);
10525 self.formula_parse_diagnostics
10526 .try_reserve(pending_diagnostics.len())
10527 .map_err(|_| {
10528 resource(
10529 formualizer_common::ResourceExhaustionReason::Admission,
10530 pending_diagnostics.len() as u64,
10531 pending_diagnostics.len() as u64,
10532 )
10533 })?;
10534 self.target_preparation_checkpoint(options.deadline, 0)?;
10535 #[cfg(test)]
10536 self.target_preparation_fault(
10537 crate::engine::target_preparation::TargetPreparationFault::BeforeFirstMutation,
10538 )?;
10539
10540 let commit_started = crate::instant::FzInstant::now();
10541 let committed = self
10542 .graph
10543 .apply_prevalidated_legacy_graph_plan(legacy_graph);
10544 for formula in &prepared {
10545 let removed = self
10546 .staged_formulas
10547 .get_mut(&formula.sheet)
10548 .and_then(|sheet| sheet.remove_ordinary(formula.lease.row, formula.lease.col));
10549 debug_assert!(removed.is_some());
10550 let index_removed = self.staged_formula_index.remove(
10551 &formula.sheet,
10552 formula.lease.row,
10553 formula.lease.col,
10554 );
10555 debug_assert!(index_removed);
10556 }
10557 for package in &prepared_packages {
10558 if let Some(points) = &package.selected_points {
10559 let staged = self.staged_formulas.get_mut(&package.sheet).unwrap();
10560 let source = staged.deferred_package.as_mut().unwrap();
10561 source.consumed_engine = Some(Arc::clone(&self.source_formula_token));
10562 source
10565 .consumed_members
10566 .extend(package.replay_records.iter().filter_map(|record| {
10567 record
10568 .partition_owner
10569 .or(record.family)
10570 .filter(|owner| residual_owners[&package.sheet].contains(owner))
10571 .map(|owner| {
10572 (
10573 owner,
10574 crate::engine::SourceCoord {
10575 row: record.row - 1,
10576 col: record.col - 1,
10577 },
10578 )
10579 })
10580 }));
10581 source.suppressed.extend(points.iter().copied());
10582 source.source_accounted = true;
10583 self.staged_formula_index
10584 .consume_package_points(&package.sheet, points);
10585 if source.suppressed.len() >= source.source_coordinates.len() {
10586 staged.deferred_package = None;
10587 self.staged_formula_index.set_package(&package.sheet, None);
10588 }
10589 } else {
10590 let removed = self
10591 .staged_formulas
10592 .get_mut(&package.sheet)
10593 .and_then(|staged| staged.deferred_package.take());
10594 debug_assert!(removed.is_some());
10595 self.staged_formula_index.set_package(&package.sheet, None);
10596 }
10597 }
10598 for (sheet, (families, partitions)) in residual_sources {
10599 if let Some(source) = self
10600 .staged_formulas
10601 .get_mut(&sheet)
10602 .and_then(|staged| staged.deferred_package.as_mut())
10603 {
10604 source.families = families;
10605 source.partitioned_families = partitions;
10606 source
10607 .consumed_members
10608 .retain(|(owner, _)| residual_owners[&sheet].contains(owner));
10609 self.staged_formula_index.update_package_family_count(
10610 &sheet,
10611 source.families.len() + source.partitioned_families.len(),
10612 );
10613 }
10614 }
10615 let empty_sheets = self
10616 .staged_formulas
10617 .iter()
10618 .filter_map(|(sheet, staged)| staged.is_empty().then_some(sheet.clone()))
10619 .collect::<Vec<_>>();
10620 for sheet in empty_sheets {
10621 self.staged_formulas.remove(&sheet);
10622 }
10623 if committed > 0 {
10624 self.mark_topology_edited();
10625 }
10626 self.formula_parse_diagnostics.extend(pending_diagnostics);
10627 if !prepared.is_empty() || !prepared_packages.is_empty() {
10628 let mut ingest_delta = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
10629 ingest_delta.formula_cells_seen = (prepared.len() as u64).saturating_add(
10630 prepared_packages
10631 .iter()
10632 .map(|package| {
10633 (package.replay_records.len() as u64).saturating_add(
10634 if package.selected_points.is_some() {
10635 package.direct_cells
10636 } else {
10637 0
10638 },
10639 )
10640 })
10641 .sum::<u64>(),
10642 );
10643 ingest_delta.graph_formula_cells_materialized = committed as u64;
10644 ingest_delta.graph_vertices_created = new_vertices as u64;
10645 ingest_delta.graph_edges_created = new_edges as u64;
10646 for package in &prepared_packages {
10647 let source = &package.source_report;
10648 ingest_delta.source_formula_events = ingest_delta
10649 .source_formula_events
10650 .saturating_add(source.source_formula_events);
10651 ingest_delta.source_formula_records_spooled = ingest_delta
10652 .source_formula_records_spooled
10653 .saturating_add(source.source_formula_records_spooled);
10654 ingest_delta.source_spool_encoded_bytes = ingest_delta
10655 .source_spool_encoded_bytes
10656 .saturating_add(source.source_spool_encoded_bytes);
10657 ingest_delta.source_spool_peak_memory_bytes = ingest_delta
10658 .source_spool_peak_memory_bytes
10659 .max(source.source_spool_peak_memory_bytes);
10660 ingest_delta.source_spool_spilled_bytes = ingest_delta
10661 .source_spool_spilled_bytes
10662 .saturating_add(source.source_spool_spilled_bytes);
10663 ingest_delta.source_spool_spill_files = ingest_delta
10664 .source_spool_spill_files
10665 .saturating_add(source.source_spool_spill_files);
10666 ingest_delta.source_spool_replays = ingest_delta
10667 .source_spool_replays
10668 .saturating_add(source.source_spool_replays)
10669 .saturating_add(package.spool_replays);
10670 ingest_delta.source_families_seen = ingest_delta
10671 .source_families_seen
10672 .saturating_add(source.families_seen);
10673 ingest_delta.source_family_cells_seen = ingest_delta
10674 .source_family_cells_seen
10675 .saturating_add(source.family_cells_seen);
10676 ingest_delta.source_family_shadow_eligible = ingest_delta
10677 .source_family_shadow_eligible
10678 .saturating_add(source.source_clean_families);
10679 ingest_delta.source_family_shadow_eligible_cells = ingest_delta
10680 .source_family_shadow_eligible_cells
10681 .saturating_add(source.source_clean_cells);
10682 ingest_delta.source_partitioned_families_seen = ingest_delta
10683 .source_partitioned_families_seen
10684 .saturating_add(source.source_fragmentable_families);
10685 ingest_delta.source_partition_holes = ingest_delta
10686 .source_partition_holes
10687 .saturating_add(source.source_hole_exclusions);
10688 ingest_delta.source_partition_ordinary_exceptions = ingest_delta
10689 .source_partition_ordinary_exceptions
10690 .saturating_add(source.source_ordinary_exclusions);
10691 ingest_delta.source_partition_surviving_cells = ingest_delta
10692 .source_partition_surviving_cells
10693 .saturating_add(source.source_fragmentable_cells);
10694 for (reason, count) in &source.fallback_reasons {
10695 let total = ingest_delta
10696 .fallback_reasons
10697 .entry(reason.clone())
10698 .or_default();
10699 *total = total.saturating_add(*count);
10700 }
10701
10702 ingest_delta.source_family_fallback = ingest_delta
10703 .source_family_fallback
10704 .saturating_add(source.families_seen);
10705 ingest_delta.source_family_fallback_cells = ingest_delta
10706 .source_family_fallback_cells
10707 .saturating_add(source.family_cells_seen);
10708 }
10709 self.record_formula_ingest_report(ingest_delta);
10710 }
10711 let commit_window = commit_started.elapsed();
10712 let retained_cells = self
10713 .staged_formula_index
10714 .all_leases()
10715 .into_iter()
10716 .filter_map(|(sheet, lease)| {
10717 formualizer_common::RangeAddress::new(
10718 sheet, lease.row, lease.col, lease.row, lease.col,
10719 )
10720 .ok()
10721 })
10722 .collect::<Vec<_>>();
10723 let observed_scratch_bytes = self
10724 .active_resource_ledger
10725 .as_ref()
10726 .map_or(0, |ledger| ledger.snapshot().scratch_peak)
10727 .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
10728 let committed_spans = 0u64;
10729 let selected_source_families = prepared_packages
10730 .iter()
10731 .filter(|package| package.selected_points.is_none())
10732 .map(|package| package.lease.family_count)
10733 .sum::<usize>()
10734 + prepared_packages
10735 .iter()
10736 .filter(|package| package.selected_points.is_some())
10737 .flat_map(|package| package.replay_records.iter())
10738 .filter_map(|record| record.partition_owner.or(record.family))
10739 .chain(
10740 prepared_packages
10741 .iter()
10742 .flat_map(|package| package.complete_selections.iter().copied()),
10743 )
10744 .collect::<BTreeSet<_>>()
10745 .len();
10746 let selected_staged_cells = prepared.len().saturating_add(
10747 prepared_packages
10748 .iter()
10749 .map(|package| {
10750 package
10751 .selected_points
10752 .as_ref()
10753 .map_or(package.replay_records.len(), BTreeSet::len)
10754 })
10755 .sum::<usize>(),
10756 );
10757 let actual_commit_work = (new_vertices as u64)
10758 .saturating_add(new_edges as u64)
10759 .saturating_add(committed as u64)
10760 .saturating_add(committed_spans)
10761 .saturating_add(prepared.len() as u64)
10762 .saturating_add(
10763 prepared_packages
10764 .iter()
10765 .map(|package| {
10766 package
10767 .selected_points
10768 .as_ref()
10769 .map_or(1, |points| points.len() as u64)
10770 })
10771 .sum::<u64>(),
10772 );
10773 let report = PreparedTargetGraphReport {
10774 request_id: request_id.unwrap_or_default(),
10775 requested_targets: targets.len(),
10776 normalized_regions: visited_regions.len(),
10777 normalized_target_list: normalized,
10778 selected_staged_cells,
10779 selected_source_families,
10780 retained_staged_cells: self.staged_formula_count(),
10781 selected_cells,
10782 retained_cells,
10783 widened_scope: scope,
10784 widening_reasons: reasons,
10785 revisions: assumptions,
10786 commit_window,
10787 estimated_scratch_bytes: scratch_bytes,
10788 observed_scratch_bytes,
10789 estimated_commit_work: (new_vertices as u64)
10790 .saturating_add(new_edges as u64)
10791 .saturating_add(prepared.len() as u64)
10792 .saturating_add(selected_package_records),
10793 actual_commit_work,
10794 outcome: PreparationOutcome::Prepared,
10795 };
10796 self.observe_target_preparation_report(&report);
10797 Ok(report)
10798 }
10799
10800 pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
10802 self.ensure_not_poisoned()?;
10803 self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
10804 engine.build_graph_all_unobserved()
10805 })
10806 }
10807
10808 fn build_graph_all_unobserved(&mut self) -> Result<(), formualizer_parse::ExcelError> {
10809 let selected = self.staged_formula_count();
10810 let started = crate::instant::FzInstant::now();
10811 self.resource_checkpoint(selected as u64)?;
10812 let scratch_bytes = (selected as u64).saturating_mul(256);
10813 let result = self.with_request_scratch(scratch_bytes, |engine| {
10814 let index_snapshot = engine.staged_formula_index.clone();
10815 let collected = std::mem::take(&mut engine.staged_formulas)
10816 .into_iter()
10817 .collect();
10818 engine.staged_formula_index.clear_all();
10819 engine.build_graph_from_staged_batches(collected, false, index_snapshot)
10820 });
10821 self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
10822 result
10823 }
10824
10825 pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
10827 &mut self,
10828 sheets: I,
10829 ) -> Result<(), formualizer_parse::ExcelError> {
10830 self.ensure_not_poisoned()?;
10831 let mut sheets = sheets.into_iter();
10832 self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, move |engine| {
10833 let name_scratch = (sheets.size_hint().0 as u64).saturating_mul(64);
10834 engine.with_request_scratch(name_scratch, |engine| {
10835 let names = sheets.by_ref().map(str::to_string).collect::<Vec<_>>();
10838 engine.charge_bounded_work(names.len() as u64)?;
10839 engine.build_graph_for_sheet_names_unobserved(names)
10840 })
10841 })
10842 }
10843
10844 fn build_graph_for_sheet_names_unobserved(
10845 &mut self,
10846 sheets: Vec<String>,
10847 ) -> Result<(), formualizer_parse::ExcelError> {
10848 let started = crate::instant::FzInstant::now();
10849 let selected = sheets
10850 .iter()
10851 .filter_map(|sheet| self.staged_formulas.get(sheet))
10852 .map(StagedSheet::len)
10853 .sum::<usize>();
10854 self.resource_checkpoint(selected as u64)?;
10855 let scratch_bytes = (selected as u64).saturating_mul(256);
10856 self.reserve_request_scratch(scratch_bytes)?;
10857 let index_snapshot = self.staged_formula_index.clone();
10858 let mut collected = Vec::new();
10859 for sheet in sheets {
10860 if let Some(staged) = self.staged_formulas.remove(&sheet) {
10861 self.index_removed_staged_sheet(&sheet, &staged);
10862 collected.push((sheet, staged));
10863 }
10864 }
10865 let result = self.build_graph_from_staged_batches(collected, true, index_snapshot);
10866 self.release_request_scratch(scratch_bytes);
10867 self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
10868 result
10869 }
10870
10871 fn build_graph_from_staged_batches(
10872 &mut self,
10873 collected: StagedFormulaBatches,
10874 share_parse_cache_across_sheets: bool,
10875 staged_index_snapshot: StagedFormulaIndex,
10876 ) -> Result<(), formualizer_parse::ExcelError> {
10877 if collected.is_empty() {
10878 return Ok(());
10879 }
10880 for (sheet, _) in &collected {
10881 let _ = self.add_sheet(sheet);
10882 }
10883
10884 let diagnostics_len = self.formula_parse_diagnostics.len();
10885 let mut collected = collected;
10886 let prepared = match self
10887 .prepare_staged_formula_batches(&mut collected, share_parse_cache_across_sheets)
10888 {
10889 Ok(prepared) => prepared,
10890 Err(error) => {
10891 self.formula_parse_diagnostics.truncate(diagnostics_len);
10892 for (sheet, staged) in collected {
10893 self.restore_staged_sheet(sheet, staged);
10894 }
10895 self.staged_formula_index = staged_index_snapshot;
10896 return Err(error);
10897 }
10898 };
10899 let (ordinary, compressed, direct, may_fail) = prepared;
10900
10901 let first_build = if self.deferred_build_can_be_first_load() {
10911 if let Err(error) = self.check_staged_formula_plans(
10912 ordinary
10913 .iter()
10914 .chain(compressed.iter().map(|(batch, _)| batch)),
10915 &may_fail,
10916 ) {
10917 self.formula_parse_diagnostics.truncate(diagnostics_len);
10918 for (sheet, staged) in collected {
10919 self.restore_staged_sheet(sheet, staged);
10920 }
10921 self.staged_formula_index = staged_index_snapshot;
10922 return Err(error);
10923 }
10924 self.deferred_build_as_first_load()
10925 } else {
10926 None
10927 };
10928 let built_sheets: Vec<String> = if first_build.is_some() {
10929 collected.iter().map(|(sheet, _)| sheet.clone()).collect()
10930 } else {
10931 Vec::new()
10932 };
10933
10934 let result = (|| {
10938 if !ordinary.is_empty() {
10939 self.ingest_formula_batches(ordinary)?;
10940 }
10941 if !compressed.is_empty() {
10942 self.ingest_compressed_formula_source_batches(compressed)?;
10943 }
10944 if !direct.is_empty() {
10945 self.finish_compressed_formula_sources(direct)?;
10946 }
10947 Ok(())
10948 })();
10949 if let Some(saved) = first_build {
10950 self.leave_deferred_first_load(saved, &built_sheets);
10951 }
10952 if let Err(error) = result {
10953 self.formula_parse_diagnostics.truncate(diagnostics_len);
10954 for (sheet, staged) in collected {
10955 self.restore_staged_sheet(sheet, staged);
10956 }
10957 self.staged_formula_index = staged_index_snapshot;
10958 return Err(error);
10959 }
10960 self.dedup_formula_parse_diagnostics_since(diagnostics_len);
10961 Ok(())
10962 }
10963
10964 fn deferred_build_can_be_first_load(&self) -> bool {
10970 !self.graph.first_load_assume_new()
10971 && !self.graph_admission_enabled()
10972 && self.graph.formula_vertex_count() == 0
10973 }
10974
10975 fn check_staged_formula_plans<'b>(
10980 &mut self,
10981 batches: impl Iterator<Item = &'b FormulaIngestBatch>,
10982 may_fail: &FxHashSet<crate::engine::arena::AstNodeId>,
10983 ) -> Result<(), ExcelError> {
10984 if may_fail.is_empty() {
10985 return Ok(());
10986 }
10987 let mut seen: FxHashSet<(SheetId, crate::engine::arena::AstNodeId)> = FxHashSet::default();
10988 for batch in batches {
10989 let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
10990 ExcelError::new(ExcelErrorKind::Ref)
10991 .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
10992 })?;
10993 let mut pipeline = self.ingest_pipeline();
10994 for record in &batch.formulas {
10995 if record.member_anchor.is_some()
10996 || !may_fail.contains(&record.ast_id)
10997 || !seen.insert((sheet_id, record.ast_id))
10998 {
10999 continue;
11000 }
11001 let placement = CellRef::new(
11002 sheet_id,
11003 Coord::from_excel(record.row, record.col, true, true),
11004 );
11005 pipeline.ingest_formula(
11006 FormulaAstInput::RawArena(record.ast_id),
11007 placement,
11008 None,
11009 )?;
11010 }
11011 }
11012 Ok(())
11013 }
11014
11015 fn deferred_build_as_first_load(&mut self) -> Option<(crate::engine::SheetIndexMode, usize)> {
11016 if !self.deferred_build_can_be_first_load() {
11017 return None;
11018 }
11019 let saved = (
11020 self.graph.get_config().sheet_index_mode,
11021 self.config.range_expansion_limit,
11022 );
11023 self.graph
11024 .set_sheet_index_mode(crate::engine::SheetIndexMode::Lazy);
11025 self.config.range_expansion_limit = 0;
11026 self.graph.set_first_load_assume_new(true);
11027 self.graph.reset_ensure_touched();
11028 Some(saved)
11029 }
11030
11031 fn leave_deferred_first_load(
11034 &mut self,
11035 (index_mode, range_limit): (crate::engine::SheetIndexMode, usize),
11036 sheets: &[String],
11037 ) {
11038 self.graph.set_first_load_assume_new(false);
11039 self.graph.reset_ensure_touched();
11040 self.graph.set_sheet_index_mode(index_mode);
11041 self.config.range_expansion_limit = range_limit;
11042 for sheet in sheets {
11043 self.graph.finalize_sheet_index(sheet);
11044 }
11045 }
11046
11047 fn prepare_staged_formula_batches(
11048 &mut self,
11049 collected: &mut StagedFormulaBatches,
11050 share_parse_cache_across_sheets: bool,
11051 ) -> Result<PreparedStagedFormulaBatches, formualizer_parse::ExcelError> {
11052 let mut ordinary = Vec::new();
11053 let mut compressed = Vec::new();
11054 let mut direct = Vec::new();
11055 let mut may_fail = FxHashSet::default();
11056 let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
11057 rustc_hash::FxHashMap::default();
11058 cache.reserve(4096);
11059
11060 for (sheet, staged) in collected {
11061 if !share_parse_cache_across_sheets {
11062 cache.clear();
11063 }
11064 let mut grouper = crate::engine::FormulaFamilyGrouper::new();
11068 let mut replayed_records = Vec::new();
11072 let deferred_source = None;
11073 let mut deferred_fallback = None;
11074 if let Some(package) = staged.deferred_package.as_mut() {
11075 if package.sheet_name != *sheet {
11076 return Err(ExcelError::new(ExcelErrorKind::Value)
11077 .with_message("deferred formula package sheet mismatch"));
11078 }
11079 let eligible: Vec<_> = package
11080 .families
11081 .iter()
11082 .filter(|family| !package.invalidated.contains(&family.source_id))
11083 .cloned()
11084 .collect();
11085 let eligible_partitions: Vec<_> = package
11086 .partitioned_families
11087 .iter()
11088 .filter(|family| !package.invalidated.contains(&family.source_id))
11089 .cloned()
11090 .collect();
11091
11092 let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
11093 for partition in &eligible_partitions {
11094 replay_disposition
11095 .register_partition(partition, false)
11096 .map_err(|reason| {
11097 ExcelError::new(ExcelErrorKind::Value).with_message(reason)
11098 })?;
11099 }
11100 replay_disposition
11101 .extend_suppressed_excel_coords(package.suppressed.iter().copied());
11102 let replayed = package
11103 .replay
11104 .lock()
11105 .map_err(|_| {
11106 ExcelError::new(ExcelErrorKind::Value)
11107 .with_message("deferred formula spool lock poisoned")
11108 })?
11109 .replay(&replay_disposition)
11110 .map_err(|message| {
11111 ExcelError::new(ExcelErrorKind::Value).with_message(message)
11112 })?;
11113 replayed_records = replayed;
11114 let mut report = package.accounting_report();
11115 report.source_spool_replays = report.source_spool_replays.saturating_add(1);
11116 deferred_fallback = Some((report, package.families.clone(), eligible_partitions));
11117 }
11118
11119 let n_entries = staged.entries.len() + replayed_records.len();
11120 let entries = staged
11121 .entries
11122 .iter()
11123 .cloned()
11124 .map(|(row, col, text)| (row, col, text, None))
11125 .chain(replayed_records.into_iter().map(|record| {
11126 (
11127 record.row,
11128 record.col,
11129 record.text,
11130 Some((record.source_order, record.family, record.partition_owner)),
11131 )
11132 }));
11133 let mut formulas = Vec::with_capacity(n_entries);
11134 let staged_order_base = u64::MAX.saturating_sub(n_entries as u64);
11135 for (entry_index, (row, col, txt, source_proof)) in entries.enumerate() {
11136 let key = if txt.starts_with('=') {
11137 txt
11138 } else {
11139 format!("={txt}")
11140 };
11141 grouper.note_formula();
11142 let shared_key = source_proof
11144 .and_then(|(_, family, _)| family)
11145 .map(|family| u64::from(family.source_index));
11146 let staged_record = if let Some(cached) = cache.get(&key) {
11147 grouper.note_parse_cache_hit();
11148 cached.map(|ast_id| {
11149 self.note_staged_formula(&mut grouper, row, col, ast_id);
11150 FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(key)))
11151 })
11152 } else if let Some(record) =
11153 self.stage_relocated_text(&mut grouper, row, col, &key, shared_key)
11154 {
11155 Some(record)
11156 } else {
11157 grouper.note_parse(key.len());
11158 let parsed = formualizer_parse::parser::parse(&key);
11159 let parsed_text = parsed.is_ok();
11160 let parsed = match parsed {
11161 Ok(parsed) => Some(parsed),
11162 Err(error) => self.handle_formula_parse_error(
11163 sheet,
11164 row,
11165 col,
11166 &key,
11167 error.to_string(),
11168 )?,
11169 };
11170 match parsed {
11171 Some(ast) => {
11172 let record = self.stage_formula_ast(&mut grouper, row, col, &ast, None);
11173 if parsed_text {
11174 self.note_parsed_text(
11175 &mut grouper,
11176 row,
11177 col,
11178 &key,
11179 &ast,
11180 &record,
11181 shared_key,
11182 );
11183 }
11184 if !record.is_family_member() && self.formula_may_fail_planning(&ast) {
11185 may_fail.insert(record.ast_id);
11186 }
11187 if record.is_family_member() {
11190 Some(record)
11191 } else {
11192 let ast_id = record.ast_id;
11193 cache.insert(key.clone(), Some(ast_id));
11194 Some(FormulaIngestRecord::new(
11195 row,
11196 col,
11197 ast_id,
11198 Some(Arc::<str>::from(key)),
11199 ))
11200 }
11201 }
11202 None => {
11203 cache.insert(key, None);
11204 None
11205 }
11206 }
11207 };
11208
11209 if let Some(mut formula) = staged_record {
11210 if let Some((order, family, owner)) = source_proof {
11211 formula = formula.with_source_proof(order, family, owner);
11212 } else if deferred_source.is_some() {
11213 formula = formula.with_source_proof(
11214 crate::engine::SourceFormulaOrder::new(
11215 staged_order_base.saturating_add(entry_index as u64),
11216 ),
11217 None,
11218 None,
11219 );
11220 }
11221 formulas.push(formula);
11222 }
11223 }
11224
11225 self.finish_family_grouper(&mut grouper);
11226 let batch = FormulaIngestBatch::new(sheet.clone(), formulas);
11227 if let Some((report, preparation)) = deferred_source {
11228 direct.push((batch, report, preparation));
11229 } else if let Some((report, families, partitions)) = deferred_fallback {
11230 let source_batch = crate::engine::FormulaCompressedSourceBatch::with_proposals(
11231 batch.sheet_name.clone(),
11232 report,
11233 families,
11234 partitions,
11235 );
11236 compressed.push((batch, source_batch));
11237 } else if !batch.is_empty() {
11238 ordinary.push(batch);
11239 }
11240 }
11241 if std::env::var("FZ_DEBUG_LOAD")
11242 .ok()
11243 .is_some_and(|v| v != "0")
11244 {
11245 eprintln!(
11246 "[fz][families] deferred build: {}",
11247 self.source_family_counters.debug_line()
11248 );
11249 }
11250 Ok((ordinary, compressed, direct, may_fail))
11251 }
11252
11253 fn formula_may_fail_planning(&self, ast: &formualizer_parse::parser::ASTNode) -> bool {
11258 use formualizer_parse::parser::{ASTNodeType, ReferenceType};
11259 let sheet_missing = |sheet: &Option<String>| {
11260 sheet
11261 .as_deref()
11262 .is_some_and(|s| self.graph.sheet_id(s).is_none())
11263 };
11264 match &ast.node_type {
11265 ASTNodeType::Literal(_) | ASTNodeType::Omitted => false,
11266 ASTNodeType::Reference { reference, .. } => match reference {
11267 ReferenceType::Cell { sheet, .. } => sheet_missing(sheet),
11268 ReferenceType::Range {
11269 sheet,
11270 start_row,
11271 start_col,
11272 end_row,
11273 end_col,
11274 ..
11275 } => {
11276 sheet_missing(sheet)
11277 || matches!((start_row, end_row), (Some(a), Some(b)) if a > b)
11278 || matches!((start_col, end_col), (Some(a), Some(b)) if a > b)
11279 }
11280 ReferenceType::NamedRange(_) => false,
11281 _ => true,
11282 },
11283 ASTNodeType::UnaryOp { expr, .. } => self.formula_may_fail_planning(expr),
11284 ASTNodeType::BinaryOp { left, right, .. } => {
11285 self.formula_may_fail_planning(left) || self.formula_may_fail_planning(right)
11286 }
11287 ASTNodeType::Function { args, .. } => {
11288 args.iter().any(|a| self.formula_may_fail_planning(a))
11289 }
11290 ASTNodeType::Call { callee, args } => {
11291 self.formula_may_fail_planning(callee)
11292 || args.iter().any(|a| self.formula_may_fail_planning(a))
11293 }
11294 ASTNodeType::Array(rows) => rows
11295 .iter()
11296 .flatten()
11297 .any(|a| self.formula_may_fail_planning(a)),
11298 }
11299 }
11300
11301 pub fn begin_bulk_ingest_arrow(
11306 &mut self,
11307 ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
11308 self.assert_not_poisoned();
11309 crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
11310 }
11311
11312 pub fn begin_bulk_update_arrow(
11317 &mut self,
11318 ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
11319 self.assert_not_poisoned();
11320 crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
11321 }
11322
11323 fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
11324 self.graph.sheet_id(sheet).ok_or(
11325 crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
11326 name: sheet.to_string(),
11327 reason: "Unknown sheet".to_string(),
11328 },
11329 )
11330 }
11331
11332 fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
11333 if row_1based == 0 {
11334 return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
11335 }
11336 Ok(row_1based - 1)
11337 }
11338
11339 fn normalize_row_range_1based(
11340 start_row_1based: u32,
11341 end_row_1based: u32,
11342 ) -> Result<(u32, u32), crate::engine::EditorError> {
11343 if start_row_1based == 0 || end_row_1based == 0 {
11344 return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
11345 }
11346 if start_row_1based > end_row_1based {
11347 return Err(crate::engine::EditorError::TransactionFailed {
11348 reason: "Row range start is greater than end".to_string(),
11349 });
11350 }
11351 Ok((start_row_1based - 1, end_row_1based - 1))
11352 }
11353
11354 fn invalidate_row_visibility_mask_cache(&self) {
11355 if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
11356 cache.clear();
11357 }
11358 }
11359
11360 fn row_visibility_changed(&mut self) {
11363 self.invalidate_row_visibility_mask_cache();
11364 self.graph.mark_row_visibility_readers_dirty();
11365 }
11366
11367 fn set_row_hidden_by_sheet_id(
11368 &mut self,
11369 sheet_id: SheetId,
11370 row0: u32,
11371 hidden: bool,
11372 source: RowVisibilitySource,
11373 ) -> bool {
11374 let changed = {
11375 let state = self.row_visibility.entry(sheet_id).or_default();
11376 state.set_row_hidden(row0, hidden, source)
11377 };
11378
11379 let remove_entry = self
11380 .row_visibility
11381 .get(&sheet_id)
11382 .map(|state| state.is_empty())
11383 .unwrap_or(false);
11384 if remove_entry {
11385 self.row_visibility.remove(&sheet_id);
11386 }
11387
11388 if changed {
11389 self.row_visibility_changed();
11390 }
11391
11392 changed
11393 }
11394
11395 fn set_rows_hidden_by_sheet_id(
11396 &mut self,
11397 sheet_id: SheetId,
11398 start_row0: u32,
11399 end_row0: u32,
11400 hidden: bool,
11401 source: RowVisibilitySource,
11402 ) -> bool {
11403 let changed = {
11404 let state = self.row_visibility.entry(sheet_id).or_default();
11405 state.set_rows_hidden(start_row0, end_row0, hidden, source)
11406 };
11407
11408 let remove_entry = self
11409 .row_visibility
11410 .get(&sheet_id)
11411 .map(|state| state.is_empty())
11412 .unwrap_or(false);
11413 if remove_entry {
11414 self.row_visibility.remove(&sheet_id);
11415 }
11416
11417 if changed {
11418 self.row_visibility_changed();
11419 }
11420
11421 changed
11422 }
11423
11424 fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
11425 if count == 0 {
11426 return;
11427 }
11428 let mut changed = false;
11429 let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
11430 changed = state.insert_rows(before0, count);
11431 state.is_empty()
11432 } else {
11433 false
11434 };
11435 if remove_entry {
11436 self.row_visibility.remove(&sheet_id);
11437 }
11438 if changed {
11439 self.row_visibility_changed();
11440 }
11441 }
11442
11443 fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
11444 if count == 0 {
11445 return;
11446 }
11447 let mut changed = false;
11448 let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
11449 changed = state.delete_rows(start0, count);
11450 state.is_empty()
11451 } else {
11452 false
11453 };
11454 if remove_entry {
11455 self.row_visibility.remove(&sheet_id);
11456 }
11457 if changed {
11458 self.row_visibility_changed();
11459 }
11460 }
11461
11462 fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
11463 if let crate::engine::ChangeEvent::SetRowVisibility {
11464 sheet_id,
11465 row0,
11466 source,
11467 old_hidden,
11468 ..
11469 } = event
11470 {
11471 let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
11472 }
11473 }
11474
11475 fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
11476 if let crate::engine::ChangeEvent::SetRowVisibility {
11477 sheet_id,
11478 row0,
11479 source,
11480 new_hidden,
11481 ..
11482 } = event
11483 {
11484 let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
11485 }
11486 }
11487
11488 fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
11489 for event in events.iter().rev() {
11490 self.apply_inverse_row_visibility_event(event);
11491 }
11492 }
11493
11494 fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
11495 for event in events {
11496 self.apply_forward_row_visibility_event(event);
11497 }
11498 }
11499
11500 fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
11501 if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
11502 sheet,
11503 row,
11504 col,
11505 old,
11506 ..
11507 } = event
11508 {
11509 self.apply_staged_formula_cell(sheet, *row, *col, old.as_deref());
11510 }
11511 }
11512
11513 fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
11514 if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
11515 sheet,
11516 row,
11517 col,
11518 new,
11519 ..
11520 } = event
11521 {
11522 self.apply_staged_formula_cell(sheet, *row, *col, new.as_deref());
11523 }
11524 }
11525
11526 fn apply_staged_formula_cell(&mut self, sheet: &str, row: u32, col: u32, target: Option<&str>) {
11529 match target {
11530 Some(text) => self.stage_formula_text(sheet, row, col, text.to_string()),
11531 None => {
11532 self.clear_staged_formula_text(sheet, row, col);
11533 }
11534 }
11535 }
11536
11537 pub fn set_row_hidden(
11538 &mut self,
11539 sheet: &str,
11540 row_1based: u32,
11541 hidden: bool,
11542 source: RowVisibilitySource,
11543 ) -> Result<(), crate::engine::EditorError> {
11544 self.ensure_not_poisoned()?;
11545 self.observe_function_semantic_epoch()
11546 .map_err(crate::engine::EditorError::Excel)?;
11547 self.observe_function_semantic_epoch()
11548 .map_err(crate::engine::EditorError::Excel)?;
11549 let sheet_id = self.ensure_known_sheet_id(sheet)?;
11550 let row0 = Self::normalize_row_1based(row_1based)?;
11551 if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
11552 self.record_structural_change(StructuralScope::Region(Region::whole_row(
11553 sheet_id, row0,
11554 )));
11555 self.mark_data_edited();
11556 }
11557 Ok(())
11558 }
11559
11560 pub fn set_rows_hidden(
11561 &mut self,
11562 sheet: &str,
11563 start_row_1based: u32,
11564 end_row_1based: u32,
11565 hidden: bool,
11566 source: RowVisibilitySource,
11567 ) -> Result<(), crate::engine::EditorError> {
11568 self.ensure_not_poisoned()?;
11569 let sheet_id = self.ensure_known_sheet_id(sheet)?;
11570 let (start_row0, end_row0) =
11571 Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
11572 if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
11573 if start_row0 == end_row0 {
11574 self.record_structural_change(StructuralScope::Region(Region::whole_row(
11575 sheet_id, start_row0,
11576 )));
11577 } else {
11578 self.record_structural_change(StructuralScope::Sheet(sheet_id));
11579 }
11580 self.mark_data_edited();
11581 }
11582 Ok(())
11583 }
11584
11585 pub fn is_row_hidden(
11586 &self,
11587 sheet: &str,
11588 row_1based: u32,
11589 source: Option<RowVisibilitySource>,
11590 ) -> Option<bool> {
11591 let sheet_id = self.graph.sheet_id(sheet)?;
11592 let row0 = row_1based.checked_sub(1)?;
11593 Some(
11594 self.row_visibility
11595 .get(&sheet_id)
11596 .map(|state| state.is_row_hidden(row0, source))
11597 .unwrap_or(false),
11598 )
11599 }
11600
11601 pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
11602 let sheet_id = self.graph.sheet_id(sheet)?;
11603 Some(
11604 self.row_visibility
11605 .get(&sheet_id)
11606 .map(|state| state.version())
11607 .unwrap_or(0),
11608 )
11609 }
11610
11611 fn nested_subtotal_cells_for_view(
11617 &self,
11618 view: &RangeView<'_>,
11619 include_aggregate: bool,
11620 ) -> Option<Vec<(usize, usize, usize)>> {
11621 if !view.is_sheet_backed() {
11622 return None;
11623 }
11624 let (rows, cols) = view.dims();
11625 if rows == 0 || cols == 0 {
11626 return Some(Vec::new());
11627 }
11628 let sheet_id = self.graph.sheet_id(view.sheet_name())?;
11629 let (r0, c0) = (view.start_row(), view.start_col());
11630 let mask = if include_aggregate {
11631 crate::engine::arena::SUBTOTAL_CALL | crate::engine::arena::AGGREGATE_CALL
11632 } else {
11633 crate::engine::arena::SUBTOTAL_CALL
11634 };
11635 let ds = self.graph.data_store();
11636 let mut found: Vec<(usize, usize, usize)> = self
11637 .graph
11638 .formula_intervals_in_region(
11639 sheet_id,
11640 r0 as u32,
11641 (r0 + rows - 1) as u32,
11642 c0 as u32,
11643 (c0 + cols - 1) as u32,
11644 |template| ds.ast_subtotal_calls(template) & mask != 0,
11645 )
11646 .into_iter()
11647 .map(|(col, lo, hi)| (col as usize - c0, lo as usize - r0, hi as usize - r0))
11648 .collect();
11649 found.sort_unstable();
11650 let mut out: Vec<(usize, usize, usize)> = Vec::with_capacity(found.len());
11653 for (col, lo, hi) in found {
11654 match out.last_mut() {
11655 Some(last) if last.0 == col && lo <= last.2 + 1 => last.2 = last.2.max(hi),
11656 _ => out.push((col, lo, hi)),
11657 }
11658 }
11659 Some(out)
11660 }
11661
11662 fn build_row_visibility_mask_for_view(
11663 &self,
11664 view: &RangeView<'_>,
11665 mode: VisibilityMaskMode,
11666 ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
11667 let sheet_rows = view.sheet().nrows as usize;
11668 if sheet_rows == 0 || view.start_row() >= sheet_rows {
11669 return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
11670 }
11671
11672 let sheet_id = self.graph.sheet_id(view.sheet_name())?;
11673 let start_row0 = view.start_row() as u32;
11674 let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
11675 let version = self
11676 .row_visibility
11677 .get(&sheet_id)
11678 .map(|state| state.version())
11679 .unwrap_or(0);
11680 let key = VisibilityMaskCacheKey {
11681 sheet_id,
11682 start_row0,
11683 end_row0,
11684 mode,
11685 version,
11686 };
11687
11688 if let Ok(cache) = self.row_visibility_mask_cache.read()
11689 && let Some(mask) = cache.get(&key)
11690 {
11691 #[cfg(test)]
11692 visibility_mask_test_hooks::inc_hit();
11693 return Some(mask.clone());
11694 }
11695
11696 #[cfg(test)]
11697 visibility_mask_test_hooks::inc_miss();
11698
11699 let state = self.row_visibility.get(&sheet_id);
11700 let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
11701 for row0 in start_row0..=end_row0 {
11702 let manual_hidden = state
11703 .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
11704 .unwrap_or(false);
11705 let filter_hidden = state
11706 .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
11707 .unwrap_or(false);
11708
11709 let include = match mode {
11710 VisibilityMaskMode::IncludeAll => true,
11711 VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
11712 VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
11713 VisibilityMaskMode::ExcludeManualOrFilterHidden => {
11714 !(manual_hidden || filter_hidden)
11715 }
11716 };
11717 out.push(include);
11718 }
11719
11720 let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
11721 if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
11722 const MAX_CACHE_ENTRIES: usize = 4096;
11723 if cache.len() >= MAX_CACHE_ENTRIES {
11724 cache.clear();
11725 #[cfg(test)]
11726 visibility_mask_test_hooks::inc_eviction();
11727 }
11728 cache.insert(key, mask.clone());
11729 }
11730
11731 Some(mask)
11732 }
11733
11734 fn observe_function_semantic_epoch(&mut self) -> Result<bool, ExcelError> {
11735 let changes =
11736 crate::function_registry::semantic_changes_since(self.function_semantic_epoch_seen);
11737 let global_changed = changes.epoch != self.function_semantic_epoch_seen;
11738 let provider_revision = self.resolver.planning_semantic_revision();
11739 let provider_changed = provider_revision != self.function_provider_revision_seen;
11740 if !global_changed && !provider_changed {
11741 return Ok(false);
11742 }
11743
11744 let changed = !changes.keys.is_empty();
11745 if global_changed && changed || provider_changed {
11746 self.cached_static_schedule = None;
11747 self.recent_schedules.clear();
11748 self.base_schedule = None;
11749 }
11750 self.function_semantic_epoch_seen = changes.epoch;
11751 self.function_provider_revision_seen = provider_revision;
11752 Ok(false)
11753 }
11754
11755 pub(crate) fn ast_uses_changed_function(
11756 ast: &ASTNode,
11757 changed: &BTreeSet<(String, String)>,
11758 ) -> bool {
11759 match &ast.node_type {
11760 ASTNodeType::Function { name, args } => {
11761 let normalized = name.to_uppercase();
11762 let mut spellings = vec![(String::new(), normalized.clone())];
11763 let mut stripped = normalized.as_str();
11764 while let Some(rest) = ["_XLFN.", "_XLL.", "_XLWS."]
11765 .iter()
11766 .find_map(|prefix| stripped.strip_prefix(prefix))
11767 {
11768 stripped = rest;
11769 spellings.push((String::new(), stripped.to_string()));
11770 }
11771 let resolved = crate::function_registry::resolve("", name);
11772 let directly_changed = spellings.iter().any(|spelling| changed.contains(spelling))
11773 || resolved.as_ref().is_some_and(|resolved| {
11774 changed.contains(&(
11775 resolved.namespace.clone(),
11776 resolved.canonical_name.clone(),
11777 ))
11778 });
11779 directly_changed
11780 || resolved.is_none()
11781 || args
11782 .iter()
11783 .any(|arg| Self::ast_uses_changed_function(arg, changed))
11784 }
11785 ASTNodeType::Call { callee, args } => {
11786 Self::ast_uses_changed_function(callee, changed)
11787 || args
11788 .iter()
11789 .any(|arg| Self::ast_uses_changed_function(arg, changed))
11790 }
11791 ASTNodeType::UnaryOp { expr, .. } => Self::ast_uses_changed_function(expr, changed),
11792 ASTNodeType::BinaryOp { left, right, .. } => {
11793 Self::ast_uses_changed_function(left, changed)
11794 || Self::ast_uses_changed_function(right, changed)
11795 }
11796 ASTNodeType::Array(rows) => rows
11797 .iter()
11798 .flatten()
11799 .any(|node| Self::ast_uses_changed_function(node, changed)),
11800 _ => false,
11801 }
11802 }
11803
11804 fn ast_contains_function(ast: &ASTNode) -> bool {
11805 match &ast.node_type {
11806 ASTNodeType::Function { .. } => true,
11807 ASTNodeType::Call { callee, args } => {
11808 Self::ast_contains_function(callee) || args.iter().any(Self::ast_contains_function)
11809 }
11810 ASTNodeType::UnaryOp { expr, .. } => Self::ast_contains_function(expr),
11811 ASTNodeType::BinaryOp { left, right, .. } => {
11812 Self::ast_contains_function(left) || Self::ast_contains_function(right)
11813 }
11814 ASTNodeType::Array(rows) => rows.iter().flatten().any(Self::ast_contains_function),
11815 ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => false,
11816 }
11817 }
11818
11819 fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
11820 Region::rows_from(sheet_id, start_row0)
11821 }
11822
11823 fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
11824 Region::cols_from(sheet_id, start_col0)
11825 }
11826
11827 #[cfg(test)]
11828 pub(crate) fn force_non_cycle_schedule_fallback_for_test(&mut self) {
11829 self.force_non_cycle_schedule_fallback_for_test = true;
11830 }
11831
11832 fn materialize_deferred_sheet_before_structural_edit(
11833 &mut self,
11834 sheet: &str,
11835 ) -> Result<(), crate::engine::EditorError> {
11836 if self.staged_formulas.contains_key(sheet) {
11837 self.build_graph_for_sheets([sheet])?;
11838 }
11839 Ok(())
11840 }
11841
11842 fn check_insert_room(
11847 &self,
11848 sheet: &str,
11849 before0: u32,
11850 count: u32,
11851 rows: bool,
11852 ) -> Result<(), crate::engine::EditorError> {
11853 let limit = if rows {
11854 crate::engine::authority::geom::MAX_ROW
11855 } else {
11856 crate::engine::authority::geom::MAX_COL
11857 } + 1;
11858 let arrow = self.arrow_sheets.sheet(sheet).map_or(0, |a| {
11859 if rows {
11860 a.nrows
11861 } else {
11862 u32::try_from(a.columns.len()).unwrap_or(u32::MAX)
11863 }
11864 });
11865 let formulas = self.graph.sheet_id(sheet).map_or(0, |id| {
11866 self.graph
11867 .grid_vertices_in_sheet(id)
11868 .map(|(_, at)| if rows { at.row() } else { at.col() } + 1)
11869 .max()
11870 .unwrap_or(0)
11871 });
11872 let extent = arrow.max(formulas);
11873 let pushed_off = extent > before0 && extent.saturating_add(count) > limit;
11874 if count > limit || pushed_off {
11875 return Err(crate::engine::EditorError::Excel(
11876 ExcelError::new(ExcelErrorKind::Ref).with_message(format!(
11877 "inserting {count} {} would push non-empty cells off the sheet",
11878 if rows { "rows" } else { "columns" }
11879 )),
11880 ));
11881 }
11882 Ok(())
11883 }
11884
11885 fn structural_row_occupancy(
11886 &self,
11887 sheet: &str,
11888 sheet_id: SheetId,
11889 ) -> crate::engine::graph::StructuralOccupancy {
11890 if !self.graph.has_compressed_range_readers() {
11891 return crate::engine::graph::StructuralOccupancy::default();
11892 }
11893 let mut occupancy = self.graph.structural_occupancy(sheet_id);
11894 if let Some(arrow_sheet) = self.arrow_sheets.sheet(sheet) {
11895 occupancy.include_arrow_sheet(arrow_sheet);
11896 occupancy
11897 } else {
11898 crate::engine::graph::StructuralOccupancy::conservative()
11900 }
11901 }
11902
11903 fn clear_spills_before_structural_edit(
11906 &mut self,
11907 sheet: SheetId,
11908 start: u32,
11909 rows: bool,
11910 capture: Option<&mut MutationCapture>,
11911 undo: Option<&mut crate::engine::ArrowUndoBatch>,
11912 ) -> Result<Vec<CellRef>, crate::engine::EditorError> {
11913 let anchors = self.graph.spill_anchors_before_shift(sheet, start, rows);
11914 if anchors.is_empty() {
11915 return Ok(Vec::new());
11916 }
11917 let cells: Vec<_> = anchors
11918 .iter()
11919 .flat_map(|&anchor| self.graph.spill_cells_to_clear(anchor))
11920 .collect();
11921 if let Some(capture) = capture {
11922 let start_len = capture.len();
11923 self.edit_with_capture(capture, |editor| {
11924 for &anchor in &anchors {
11925 editor.clear_spill_region(anchor);
11926 }
11927 })?;
11928 if let Some(undo) = undo {
11929 self.record_spill_ops_into_arrow_undo(undo, &capture.events()[start_len..]);
11930 }
11931 } else {
11932 for &anchor in &anchors {
11933 self.clear_spill_projection_and_mirror(anchor, None);
11934 }
11935 }
11936 self.graph.mark_dirty_many(&anchors);
11937 Ok(cells)
11938 }
11939
11940 fn restore_shifted_spill_tombstones(
11943 &mut self,
11944 cells: Vec<CellRef>,
11945 start: u32,
11946 count: u32,
11947 rows: bool,
11948 insert: bool,
11949 mut undo: Option<&mut crate::engine::ArrowUndoBatch>,
11950 ) {
11951 for cell in cells {
11952 let old = if rows {
11953 cell.coord.row()
11954 } else {
11955 cell.coord.col()
11956 };
11957 let shifted = if old < start {
11958 old
11959 } else if insert {
11960 old.saturating_add(count)
11961 } else if old < start.saturating_add(count) {
11962 continue;
11963 } else {
11964 old - count
11965 };
11966 let (row, col) = if rows {
11967 (shifted, cell.coord.col())
11968 } else {
11969 (cell.coord.row(), shifted)
11970 };
11971 let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
11972 if let Some(undo) = undo.as_deref_mut() {
11973 let old = self.read_computed_overlay_cell(&sheet, row + 1, col + 1);
11974 undo.record_computed_cell(cell.sheet_id, row, col, old, Some(LiteralValue::Empty));
11975 }
11976 self.mirror_value_to_computed_overlay(&sheet, row + 1, col + 1, &LiteralValue::Empty);
11977 }
11978 }
11979
11980 fn structural_column_occupancy(&self) -> crate::engine::graph::StructuralOccupancy {
11981 crate::engine::graph::StructuralOccupancy::conservative()
11985 }
11986
11987 pub fn insert_rows(
11989 &mut self,
11990 sheet: &str,
11991 before: u32,
11992 count: u32,
11993 ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
11994 {
11995 self.ensure_not_poisoned()?;
11996 if count == 0 {
11997 return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
11998 }
11999 self.observe_function_semantic_epoch()
12000 .map_err(crate::engine::EditorError::Excel)?;
12001 use crate::engine::graph::editor::vertex_editor::VertexEditor;
12002 self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12003 let sheet_id = self.ensure_known_sheet_id(sheet)?;
12004 let before0 = before.saturating_sub(1);
12005 self.check_insert_room(sheet, before0, count, true)?;
12006 let affected_region = Self::structural_row_region(sheet_id, before0);
12007 let occupancy = self.structural_row_occupancy(sheet, sheet_id);
12008 let cleared_spill_cells =
12009 self.clear_spills_before_structural_edit(sheet_id, before0, true, None, None)?;
12010 let summary = {
12011 let mut editor =
12012 VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12013 editor.insert_rows(sheet_id, before0, count)?
12014 };
12015 if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12016 let before0 = before0 as usize;
12017 asheet.insert_rows(before0, count as usize);
12018 }
12019 self.purge_derived_formats_after_row(sheet_id, before0);
12020 self.mark_moved_formula_vertices_dirty(&summary);
12021 self.clear_computed_overlay_after_row(sheet, before0 as usize);
12022 self.restore_shifted_spill_tombstones(
12023 cleared_spill_cells,
12024 before0,
12025 count,
12026 true,
12027 true,
12028 None,
12029 );
12030 self.shift_row_visibility_insert(sheet_id, before0, count);
12031 self.record_structural_change(StructuralScope::Region(affected_region));
12032 self.mark_topology_edited();
12033 Ok(summary)
12034 }
12035
12036 pub fn delete_rows(
12038 &mut self,
12039 sheet: &str,
12040 start: u32,
12041 count: u32,
12042 ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12043 {
12044 self.ensure_not_poisoned()?;
12045 if count == 0 {
12046 return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12047 }
12048 self.observe_function_semantic_epoch()
12049 .map_err(crate::engine::EditorError::Excel)?;
12050 use crate::engine::graph::editor::vertex_editor::VertexEditor;
12051 self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12052 let sheet_id = self.ensure_known_sheet_id(sheet)?;
12053 let start0 = start.saturating_sub(1);
12054 let affected_region = Self::structural_row_region(sheet_id, start0);
12055 let occupancy = self.structural_row_occupancy(sheet, sheet_id);
12056 let cleared_spill_cells =
12057 self.clear_spills_before_structural_edit(sheet_id, start0, true, None, None)?;
12058 let summary = {
12059 let mut editor =
12060 VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12061 editor.delete_rows(sheet_id, start0, count)?
12062 };
12063 if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12064 let start0 = start0 as usize;
12065 asheet.delete_rows(start0, count as usize);
12066 }
12067 self.purge_derived_formats_after_row(sheet_id, start0);
12068 self.mark_moved_formula_vertices_dirty(&summary);
12069 self.clear_computed_overlay_after_row(sheet, start0 as usize);
12070 self.restore_shifted_spill_tombstones(
12071 cleared_spill_cells,
12072 start0,
12073 count,
12074 true,
12075 false,
12076 None,
12077 );
12078 self.shift_row_visibility_delete(sheet_id, start0, count);
12079 self.record_structural_change(StructuralScope::Region(affected_region));
12080 self.mark_topology_edited();
12081 Ok(summary)
12082 }
12083
12084 pub fn insert_columns(
12086 &mut self,
12087 sheet: &str,
12088 before: u32,
12089 count: u32,
12090 ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12091 {
12092 self.ensure_not_poisoned()?;
12093 if count == 0 {
12094 return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12095 }
12096 self.observe_function_semantic_epoch()
12097 .map_err(crate::engine::EditorError::Excel)?;
12098 use crate::engine::graph::editor::vertex_editor::VertexEditor;
12099 self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12100 let sheet_id = self.graph.sheet_id(sheet).ok_or(
12101 crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
12102 name: sheet.to_string(),
12103 reason: "Unknown sheet".to_string(),
12104 },
12105 )?;
12106 let before0 = before.saturating_sub(1);
12107 self.check_insert_room(sheet, before0, count, false)?;
12108 let affected_region = Self::structural_col_region(sheet_id, before0);
12109 let occupancy = self.structural_column_occupancy();
12110 let cleared_spill_cells =
12111 self.clear_spills_before_structural_edit(sheet_id, before0, false, None, None)?;
12112 let summary = {
12113 let mut editor =
12114 VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12115 editor.insert_columns(sheet_id, before0, count)?
12116 };
12117 if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12118 let before0 = before0 as usize;
12119 asheet.insert_columns(before0, count as usize);
12120 }
12121 self.purge_derived_formats_after_col(sheet_id, before0);
12122 self.mark_moved_formula_vertices_dirty(&summary);
12123 self.clear_computed_overlay_after_col(sheet, before0 as usize);
12124 self.restore_shifted_spill_tombstones(
12125 cleared_spill_cells,
12126 before0,
12127 count,
12128 false,
12129 true,
12130 None,
12131 );
12132 self.record_structural_change(StructuralScope::Region(affected_region));
12133 self.mark_topology_edited();
12134 Ok(summary)
12135 }
12136
12137 pub fn delete_columns(
12139 &mut self,
12140 sheet: &str,
12141 start: u32,
12142 count: u32,
12143 ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12144 {
12145 self.ensure_not_poisoned()?;
12146 if count == 0 {
12147 return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12148 }
12149 self.observe_function_semantic_epoch()
12150 .map_err(crate::engine::EditorError::Excel)?;
12151 use crate::engine::graph::editor::vertex_editor::VertexEditor;
12152 self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12153 let sheet_id = self.graph.sheet_id(sheet).ok_or(
12154 crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
12155 name: sheet.to_string(),
12156 reason: "Unknown sheet".to_string(),
12157 },
12158 )?;
12159 let start0 = start.saturating_sub(1);
12160 let affected_region = Self::structural_col_region(sheet_id, start0);
12161 let occupancy = self.structural_column_occupancy();
12162 let cleared_spill_cells =
12163 self.clear_spills_before_structural_edit(sheet_id, start0, false, None, None)?;
12164 let summary = {
12165 let mut editor =
12166 VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12167 editor.delete_columns(sheet_id, start0, count)?
12168 };
12169 if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12170 let start0 = start0 as usize;
12171 asheet.delete_columns(start0, count as usize);
12172 }
12173 self.purge_derived_formats_after_col(sheet_id, start0);
12174 self.mark_moved_formula_vertices_dirty(&summary);
12175 self.clear_computed_overlay_after_col(sheet, start0 as usize);
12176 self.restore_shifted_spill_tombstones(
12177 cleared_spill_cells,
12178 start0,
12179 count,
12180 false,
12181 false,
12182 None,
12183 );
12184 self.record_structural_change(StructuralScope::Region(affected_region));
12185 self.mark_topology_edited();
12186 Ok(summary)
12187 }
12188 fn arrow_used_row_bounds(
12190 &self,
12191 sheet: &str,
12192 start_col: u32,
12193 end_col: u32,
12194 ) -> Option<(u32, u32)> {
12195 let a = self.sheet_store().sheet(sheet)?;
12196 if a.columns.is_empty() {
12197 return None;
12198 }
12199 let sc0 = start_col.saturating_sub(1) as usize;
12200 let ec0 = end_col.saturating_sub(1) as usize;
12201 let col_hi = a.columns.len().saturating_sub(1);
12202 if sc0 > col_hi {
12203 return None;
12204 }
12205 let ec0 = ec0.min(col_hi);
12206 let snap = self.data_snapshot_id();
12208 let mut min_r0: Option<usize> = None;
12209 for ci in sc0..=ec0 {
12210 let sheet_id = self.graph.sheet_id(sheet)?;
12211 if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
12212 g.as_ref()
12213 .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
12214 }) {
12215 let mv = mv as usize;
12216 min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
12217 continue;
12218 }
12219 let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
12221 if let Ok(mut g) = self.row_bounds_cache.write() {
12222 g.get_or_insert_with(|| RowBoundsCache::new(snap))
12223 .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
12224 }
12225 if let Some(m) = min_c {
12226 min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
12227 }
12228 }
12229 min_r0?;
12230 let mut max_r0: Option<usize> = None;
12231 for ci in sc0..=ec0 {
12232 let sheet_id = self.graph.sheet_id(sheet)?;
12233 if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
12234 g.as_ref()
12235 .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
12236 }) {
12237 let mv = mv as usize;
12238 max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
12239 continue;
12240 }
12241 let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
12242 if let Ok(mut g) = self.row_bounds_cache.write() {
12243 g.get_or_insert_with(|| RowBoundsCache::new(snap))
12244 .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
12245 }
12246 if let Some(m) = max_c {
12247 max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
12248 }
12249 }
12250 match (min_r0, max_r0) {
12251 (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
12252 _ => None,
12253 }
12254 }
12255
12256 fn scan_column_used_bounds(
12257 a: &crate::arrow_store::ArrowSheet,
12258 ci: usize,
12259 ) -> (Option<u32>, Option<u32>) {
12260 let col = &a.columns[ci];
12261
12262 let mut min_r0: Option<u32> = None;
12264 for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
12265 let tags = chunk.type_tag.values();
12266 for (off, &t) in tags.iter().enumerate() {
12267 let overlay_non_empty = chunk
12268 .overlay
12269 .get(off)
12270 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12271 .unwrap_or(false)
12272 || chunk
12273 .computed_overlay
12274 .get(off)
12275 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12276 .unwrap_or(false);
12277 if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12278 let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12279 break;
12280 };
12281 let row0 = chunk_start + off;
12282 min_r0 = Some(row0 as u32);
12283 break;
12284 }
12285 }
12286 if min_r0.is_some() {
12287 break;
12288 }
12289 }
12290 if min_r0.is_none() && !col.sparse_chunks.is_empty() {
12291 let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
12292 sparse_idxs.sort_unstable();
12293 for chunk_idx in sparse_idxs {
12294 let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
12295 continue;
12296 };
12297 let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12298 continue;
12299 };
12300 let tags = chunk.type_tag.values();
12301 for (off, &t) in tags.iter().enumerate() {
12302 let overlay_non_empty = chunk
12303 .overlay
12304 .get(off)
12305 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12306 .unwrap_or(false)
12307 || chunk
12308 .computed_overlay
12309 .get(off)
12310 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12311 .unwrap_or(false);
12312 if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12313 let row0 = chunk_start + off;
12314 min_r0 = Some(row0 as u32);
12315 break;
12316 }
12317 }
12318 if min_r0.is_some() {
12319 break;
12320 }
12321 }
12322 }
12323
12324 let mut max_r0: Option<u32> = None;
12326 if !col.sparse_chunks.is_empty() {
12327 let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
12328 sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
12329 for chunk_idx in sparse_idxs {
12330 let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
12331 continue;
12332 };
12333 let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12334 continue;
12335 };
12336 let tags = chunk.type_tag.values();
12337 for (rev_idx, &t) in tags.iter().enumerate().rev() {
12338 let overlay_non_empty = chunk
12339 .overlay
12340 .get(rev_idx)
12341 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12342 .unwrap_or(false)
12343 || chunk
12344 .computed_overlay
12345 .get(rev_idx)
12346 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12347 .unwrap_or(false);
12348 if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12349 let row0 = chunk_start + rev_idx;
12350 max_r0 = Some(row0 as u32);
12351 break;
12352 }
12353 }
12354 if max_r0.is_some() {
12355 break;
12356 }
12357 }
12358 }
12359 if max_r0.is_none() {
12360 for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
12361 let tags = chunk.type_tag.values();
12362 for (rev_idx, &t) in tags.iter().enumerate().rev() {
12363 let overlay_non_empty = chunk
12364 .overlay
12365 .get(rev_idx)
12366 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12367 .unwrap_or(false)
12368 || chunk
12369 .computed_overlay
12370 .get(rev_idx)
12371 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12372 .unwrap_or(false);
12373 if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12374 let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12375 break;
12376 };
12377 let row0 = chunk_start + rev_idx;
12378 max_r0 = Some(row0 as u32);
12379 break;
12380 }
12381 }
12382 if max_r0.is_some() {
12383 break;
12384 }
12385 }
12386 }
12387
12388 (min_r0, max_r0)
12389 }
12390
12391 fn arrow_used_col_bounds(
12393 &self,
12394 sheet: &str,
12395 start_row: u32,
12396 end_row: u32,
12397 ) -> Option<(u32, u32)> {
12398 let a = self.sheet_store().sheet(sheet)?;
12399 if a.columns.is_empty() {
12400 return None;
12401 }
12402 let sr0 = start_row.saturating_sub(1) as usize;
12403 let er0 = end_row.saturating_sub(1) as usize;
12404 if sr0 > er0 {
12405 return None;
12406 }
12407 let mut min_c0: Option<usize> = None;
12410 let mut max_c0: Option<usize> = None;
12411 for (ci, col) in a.columns.iter().enumerate() {
12413 let mut any_in_range = false;
12414
12415 let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
12416 let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12417 return false;
12418 };
12419 let chunk_len = chunk.type_tag.len();
12420 if chunk_len == 0 {
12421 return false;
12422 }
12423 let chunk_end = chunk_start + chunk_len.saturating_sub(1);
12424 if sr0 > chunk_end || er0 < chunk_start {
12426 return false;
12427 }
12428 let start_off = sr0.max(chunk_start) - chunk_start;
12429 let end_off = er0.min(chunk_end) - chunk_start;
12430 let tags = chunk.type_tag.values();
12431 for off in start_off..=end_off {
12432 let overlay_non_empty = chunk
12433 .overlay
12434 .get(off)
12435 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12436 .unwrap_or(false)
12437 || chunk
12438 .computed_overlay
12439 .get(off)
12440 .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12441 .unwrap_or(false);
12442 if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
12443 return true;
12444 }
12445 }
12446 false
12447 };
12448
12449 for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
12450 if scan_chunk(chunk_idx, chunk) {
12451 any_in_range = true;
12452 break;
12453 }
12454 }
12455
12456 if !any_in_range && !col.sparse_chunks.is_empty() {
12457 for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
12458 if scan_chunk(chunk_idx, chunk) {
12459 any_in_range = true;
12460 break;
12461 }
12462 }
12463 }
12464
12465 if any_in_range {
12466 min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
12467 max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
12468 }
12469 }
12470 match (min_c0, max_c0) {
12471 (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
12472 _ => None,
12473 }
12474 }
12475
12476 fn formula_row_bounds_for_columns(
12477 &self,
12478 sheet: &str,
12479 start_col: u32,
12480 end_col: u32,
12481 ) -> Option<(u32, u32)> {
12482 let sheet_id = self.graph.sheet_id(sheet)?;
12483 let sc0 = start_col.saturating_sub(1);
12484 let ec0 = end_col.saturating_sub(1);
12485 let mut min_r0: Option<u32> = None;
12486 let mut max_r0: Option<u32> = None;
12487
12488 if self.graph.sheet_index(sheet_id).is_some() {
12489 for vid in self.graph.vertices_in_cols(sheet_id, sc0, ec0) {
12490 if !matches!(
12491 self.graph.get_vertex_kind(vid),
12492 VertexKind::FormulaScalar | VertexKind::FormulaArray
12493 ) {
12494 continue;
12495 }
12496 let Some(row0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.row()) else {
12497 continue;
12498 };
12499 min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
12500 max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
12501 }
12502 } else {
12503 for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
12504 if !matches!(
12505 self.graph.get_vertex_kind(vid),
12506 VertexKind::FormulaScalar | VertexKind::FormulaArray
12507 ) {
12508 continue;
12509 }
12510 let col0 = coord.col();
12511 if col0 < sc0 || col0 > ec0 {
12512 continue;
12513 }
12514 let row0 = coord.row();
12515 min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
12516 max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
12517 }
12518 }
12519
12520 match (min_r0, max_r0) {
12521 (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
12522 _ => None,
12523 }
12524 }
12525
12526 fn formula_col_bounds_for_rows(
12527 &self,
12528 sheet: &str,
12529 start_row: u32,
12530 end_row: u32,
12531 ) -> Option<(u32, u32)> {
12532 let sheet_id = self.graph.sheet_id(sheet)?;
12533 let sr0 = start_row.saturating_sub(1);
12534 let er0 = end_row.saturating_sub(1);
12535 let mut min_c0: Option<u32> = None;
12536 let mut max_c0: Option<u32> = None;
12537
12538 if self.graph.sheet_index(sheet_id).is_some() {
12539 for vid in self.graph.vertices_in_rows(sheet_id, sr0, er0) {
12540 if !matches!(
12541 self.graph.get_vertex_kind(vid),
12542 VertexKind::FormulaScalar | VertexKind::FormulaArray
12543 ) {
12544 continue;
12545 }
12546 let Some(col0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.col()) else {
12547 continue;
12548 };
12549 min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
12550 max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
12551 }
12552 } else {
12553 for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
12554 if !matches!(
12555 self.graph.get_vertex_kind(vid),
12556 VertexKind::FormulaScalar | VertexKind::FormulaArray
12557 ) {
12558 continue;
12559 }
12560 let row0 = coord.row();
12561 if row0 < sr0 || row0 > er0 {
12562 continue;
12563 }
12564 let col0 = coord.col();
12565 min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
12566 max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
12567 }
12568 }
12569
12570 match (min_c0, max_c0) {
12571 (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
12572 _ => None,
12573 }
12574 }
12575
12576 fn union_used_bounds(
12577 first: Option<(u32, u32)>,
12578 second: Option<(u32, u32)>,
12579 ) -> Option<(u32, u32)> {
12580 match (first, second) {
12581 (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
12582 (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
12583 (None, None) => None,
12584 }
12585 }
12586
12587 fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
12590 if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12591 return;
12592 }
12593 if self.arrow_sheets.sheet(sheet).is_none() {
12594 self.arrow_sheets
12595 .sheets
12596 .push(crate::arrow_store::ArrowSheet {
12597 name: std::sync::Arc::<str>::from(sheet),
12598 date_system: self.config.date_system,
12599 columns: Vec::new(),
12600 nrows: 0,
12601 chunk_starts: Vec::new(),
12602 chunk_rows: 32 * 1024,
12603 });
12604 }
12605
12606 let row0 = row.saturating_sub(1) as usize;
12607 let col0 = col.saturating_sub(1) as usize;
12608
12609 let asheet = self
12610 .arrow_sheets
12611 .sheet_mut(sheet)
12612 .expect("ArrowSheet must exist");
12613
12614 let cur_cols = asheet.columns.len();
12615 if col0 >= cur_cols {
12616 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12617 }
12618
12619 if row0 >= asheet.nrows as usize {
12620 if asheet.columns.is_empty() {
12621 asheet.insert_columns(0, 1);
12622 }
12623 asheet.ensure_row_capacity(row0 + 1);
12624 }
12625 if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
12626 let ov =
12627 crate::arrow_store::OverlayValue::from_literal_value(value, asheet.date_system);
12628 let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
12629 let _ = ch.overlay.set(in_off, ov);
12630 let format = match value {
12631 LiteralValue::Date(_) => Some(crate::format::FormatId::DATE),
12632 LiteralValue::DateTime(_) => Some(crate::format::FormatId::DATETIME),
12633 LiteralValue::Time(_) => Some(crate::format::FormatId::TIME),
12634 LiteralValue::Duration(_) => Some(crate::format::FormatId::DURATION),
12635 _ => None,
12636 };
12637 ch.overlay.set_format(in_off, format);
12638 ch.computed_overlay.remove(in_off)
12643 } else {
12644 return;
12645 };
12646 let abs_threshold = 1024usize;
12648 let frac_den = 50usize;
12649 let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
12650 if freed > 0 {
12651 self.overlay_compactions = self.overlay_compactions.saturating_add(1);
12652 }
12653 self.adjust_computed_overlay_bytes(computed_delta);
12654 }
12655 }
12656
12657 fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
12662 if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12663 return;
12664 }
12665 let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12666 return;
12667 };
12668 let row0 = row.saturating_sub(1) as usize;
12669 let col0 = col.saturating_sub(1) as usize;
12670 if row0 >= asheet.nrows as usize {
12671 return;
12672 }
12673 if col0 >= asheet.columns.len() {
12674 return;
12675 }
12676 let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12677 return;
12678 };
12679 if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
12680 let _ = ch.overlay.remove(in_off);
12681 }
12682 }
12683
12684 fn journal_structural_computed_clear(
12687 &self,
12688 sheet: &str,
12689 start: usize,
12690 rows: bool,
12691 undo: &mut crate::engine::ArrowUndoBatch,
12692 ) {
12693 let Some(sheet_id) = self.graph.sheet_id(sheet) else {
12694 return;
12695 };
12696 let Some(asheet) = self.arrow_sheets.sheet(sheet) else {
12697 return;
12698 };
12699 for (col, column) in asheet.columns.iter().enumerate() {
12700 if !rows && col < start {
12701 continue;
12702 }
12703 for (chunk, data) in column.chunks.iter().enumerate().chain(
12704 column
12705 .sparse_chunks
12706 .iter()
12707 .map(|(chunk, data)| (*chunk, data)),
12708 ) {
12709 let Some(&row0) = asheet.chunk_starts.get(chunk) else {
12710 continue;
12711 };
12712 for (off, value) in data.computed_overlay.iter() {
12713 let row = row0.saturating_add(off);
12714 if rows && row < start {
12715 continue;
12716 }
12717 undo.record_computed_cell(
12718 sheet_id,
12719 row as u32,
12720 col as u32,
12721 Some(value.to_literal_for(asheet.date_system)),
12722 None,
12723 );
12724 }
12725 }
12726 }
12727 }
12728
12729 fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
12730 if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
12731 return;
12732 }
12733
12734 let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12735 return;
12736 };
12737 if start_row0 >= asheet.nrows as usize {
12738 return;
12739 }
12740
12741 let starts = asheet.chunk_starts.clone();
12742 let nrows = asheet.nrows as usize;
12743 let mut delta = 0isize;
12744 for col in &mut asheet.columns {
12745 for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
12746 let Some(&chunk_start) = starts.get(chunk_idx) else {
12747 continue;
12748 };
12749 let chunk_end = starts
12750 .get(chunk_idx + 1)
12751 .copied()
12752 .unwrap_or(nrows)
12753 .min(chunk_start.saturating_add(ch.len()));
12754 if chunk_end <= start_row0 {
12755 continue;
12756 }
12757 if chunk_start >= start_row0 {
12758 delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12759 } else {
12760 let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
12761 delta = delta
12762 .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
12763 }
12764 }
12765
12766 for (chunk_idx, ch) in &mut col.sparse_chunks {
12767 let Some(&chunk_start) = starts.get(*chunk_idx) else {
12768 continue;
12769 };
12770 let chunk_end = starts
12771 .get(*chunk_idx + 1)
12772 .copied()
12773 .unwrap_or(nrows)
12774 .min(chunk_start.saturating_add(ch.len()));
12775 if chunk_end <= start_row0 {
12776 continue;
12777 }
12778 if chunk_start >= start_row0 {
12779 delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12780 } else {
12781 let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
12782 delta = delta
12783 .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
12784 }
12785 }
12786 }
12787 self.adjust_computed_overlay_bytes(delta);
12788 }
12789
12790 fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
12791 if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
12792 return;
12793 }
12794
12795 let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12796 return;
12797 };
12798 if start_col0 >= asheet.columns.len() {
12799 return;
12800 }
12801
12802 let mut delta = 0isize;
12803 for col in asheet.columns.iter_mut().skip(start_col0) {
12804 for ch in &mut col.chunks {
12805 delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12806 }
12807 for ch in col.sparse_chunks.values_mut() {
12808 delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12809 }
12810 }
12811 self.adjust_computed_overlay_bytes(delta);
12812 }
12813
12814 #[inline]
12815 fn literal_to_overlay_value(
12816 value: &LiteralValue,
12817 date_system: crate::engine::DateSystem,
12818 ) -> crate::arrow_store::OverlayValue {
12819 crate::arrow_store::OverlayValue::from_literal_value(value, date_system)
12820 }
12821
12822 fn arrow_sheet_date_system(&self, sheet: &str) -> crate::engine::DateSystem {
12823 self.arrow_sheets
12824 .sheet(sheet)
12825 .map(|sheet| sheet.date_system)
12826 .unwrap_or(self.config.date_system)
12827 }
12828
12829 fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
12832 if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12833 return None;
12834 }
12835 let asheet = self.arrow_sheets.sheet(sheet)?;
12836 let row0 = row.saturating_sub(1) as usize;
12837 let col0 = col.saturating_sub(1) as usize;
12838 if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
12839 return None;
12840 }
12841 let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
12842 let ch = asheet.columns[col0].chunk(ch_idx)?;
12843 ch.overlay
12844 .get_scalar(in_off)
12845 .map(|ov| ov.to_literal_for(asheet.date_system))
12846 }
12847
12848 fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
12851 if !(self.config.arrow_storage_enabled
12852 && self.config.delta_overlay_enabled
12853 && self.config.write_formula_overlay_enabled)
12854 {
12855 return None;
12856 }
12857 let asheet = self.arrow_sheets.sheet(sheet)?;
12858 let row0 = row.saturating_sub(1) as usize;
12859 let col0 = col.saturating_sub(1) as usize;
12860 if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
12861 return None;
12862 }
12863 let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
12864 let ch = asheet.columns[col0].chunk(ch_idx)?;
12865 ch.computed_overlay
12866 .get_scalar(in_off)
12867 .map(|ov| ov.to_literal_for(asheet.date_system))
12868 }
12869
12870 fn set_delta_overlay_cell_raw(
12871 &mut self,
12872 sheet: &str,
12873 row: u32,
12874 col: u32,
12875 value: Option<LiteralValue>,
12876 ) {
12877 if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12878 return;
12879 }
12880
12881 self.ensure_arrow_sheet(sheet);
12882 let date_system = self.arrow_sheet_date_system(sheet);
12883 let ov_opt = value
12884 .as_ref()
12885 .map(|value| Self::literal_to_overlay_value(value, date_system));
12886 let row0 = row.saturating_sub(1) as usize;
12887 let col0 = col.saturating_sub(1) as usize;
12888 let asheet = self
12889 .arrow_sheets
12890 .sheet_mut(sheet)
12891 .expect("ArrowSheet must exist");
12892
12893 let cur_cols = asheet.columns.len();
12894 if col0 >= cur_cols {
12895 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12896 }
12897 if row0 >= asheet.nrows as usize {
12898 if asheet.columns.is_empty() {
12899 asheet.insert_columns(0, 1);
12900 }
12901 asheet.ensure_row_capacity(row0 + 1);
12902 }
12903
12904 let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12905 return;
12906 };
12907 let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12908 return;
12909 };
12910
12911 if let Some(ov) = ov_opt {
12912 let _ = ch.overlay.set(in_off, ov);
12913 } else {
12914 let _ = ch.overlay.remove(in_off);
12915 }
12916 }
12917
12918 fn set_computed_overlay_cell_raw(
12919 &mut self,
12920 sheet: &str,
12921 row: u32,
12922 col: u32,
12923 value: Option<LiteralValue>,
12924 ) {
12925 if !(self.config.arrow_storage_enabled
12926 && self.config.delta_overlay_enabled
12927 && self.config.write_formula_overlay_enabled)
12928 {
12929 return;
12930 }
12931
12932 self.ensure_arrow_sheet(sheet);
12933 let date_system = self.arrow_sheet_date_system(sheet);
12934 let ov_opt = value
12935 .as_ref()
12936 .map(|value| Self::literal_to_overlay_value(value, date_system));
12937 let row0 = row.saturating_sub(1) as usize;
12938 let col0 = col.saturating_sub(1) as usize;
12939 let asheet = self
12940 .arrow_sheets
12941 .sheet_mut(sheet)
12942 .expect("ArrowSheet must exist");
12943
12944 let cur_cols = asheet.columns.len();
12945 if col0 >= cur_cols {
12946 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12947 }
12948 if row0 >= asheet.nrows as usize {
12949 if asheet.columns.is_empty() {
12950 asheet.insert_columns(0, 1);
12951 }
12952 asheet.ensure_row_capacity(row0 + 1);
12953 }
12954
12955 let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12956 return;
12957 };
12958 let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12959 return;
12960 };
12961
12962 let delta = if let Some(ov) = ov_opt {
12963 ch.computed_overlay.set(in_off, ov)
12964 } else {
12965 ch.computed_overlay.remove(in_off)
12966 };
12967 self.adjust_computed_overlay_bytes(delta);
12968 }
12969
12970 fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
12971 use crate::engine::ArrowOp;
12972
12973 let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
12974 Box::new(batch.ops.iter().rev())
12975 } else {
12976 Box::new(batch.ops.iter())
12977 };
12978
12979 for op in iter {
12980 match op {
12981 ArrowOp::SetDeltaCell {
12982 sheet_id,
12983 row0,
12984 col0,
12985 old,
12986 new,
12987 } => {
12988 let sheet = self.graph.sheet_name(*sheet_id).to_string();
12989 let restored_projection = undo
12994 && self
12995 .graph
12996 .spill_registry_anchor_for_cell(CellRef::new(
12997 *sheet_id,
12998 Coord::new(*row0, *col0, true, true),
12999 ))
13000 .is_some();
13001 let v = if restored_projection {
13002 None
13003 } else if undo {
13004 old.clone()
13005 } else {
13006 new.clone()
13007 };
13008 self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
13009 }
13010 ArrowOp::SetComputedCell {
13011 sheet_id,
13012 row0,
13013 col0,
13014 old,
13015 new,
13016 } => {
13017 let sheet = self.graph.sheet_name(*sheet_id).to_string();
13018 let v = if undo { old.clone() } else { new.clone() };
13019 self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
13020 }
13021 ArrowOp::RestoreComputedRect {
13022 sheet_id,
13023 sr0,
13024 sc0,
13025 er0,
13026 ec0,
13027 old,
13028 new,
13029 } => {
13030 let sheet = self.graph.sheet_name(*sheet_id).to_string();
13031 let vals = if undo { old } else { new };
13032 let height = (*er0).saturating_sub(*sr0) as usize + 1;
13033 let width = (*ec0).saturating_sub(*sc0) as usize + 1;
13034 for r in 0..height {
13035 for c in 0..width {
13036 let v = vals
13037 .get(r)
13038 .and_then(|row| row.get(c))
13039 .cloned()
13040 .unwrap_or(LiteralValue::Empty);
13041 self.set_computed_overlay_cell_raw(
13042 &sheet,
13043 *sr0 + 1 + r as u32,
13044 *sc0 + 1 + c as u32,
13045 Some(v),
13046 );
13047 }
13048 }
13049 }
13050 ArrowOp::InsertRows {
13051 sheet_id,
13052 before0,
13053 count,
13054 } => {
13055 let sheet = self.graph.sheet_name(*sheet_id).to_string();
13056 self.ensure_arrow_sheet(&sheet);
13057 if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
13058 if undo {
13059 asheet.delete_rows(*before0 as usize, *count as usize);
13060 } else {
13061 asheet.insert_rows(*before0 as usize, *count as usize);
13062 }
13063 }
13064 self.purge_derived_formats_after_row(*sheet_id, *before0);
13065 }
13066 ArrowOp::InsertCols {
13067 sheet_id,
13068 before0,
13069 count,
13070 } => {
13071 let sheet = self.graph.sheet_name(*sheet_id).to_string();
13072 self.ensure_arrow_sheet(&sheet);
13073 if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
13074 if undo {
13075 asheet.delete_columns(*before0 as usize, *count as usize);
13076 } else {
13077 asheet.insert_columns(*before0 as usize, *count as usize);
13078 }
13079 }
13080 self.purge_derived_formats_after_col(*sheet_id, *before0);
13081 }
13082 }
13083 }
13084 }
13085
13086 fn record_spill_ops_into_arrow_undo(
13087 &mut self,
13088 undo: &mut crate::engine::ArrowUndoBatch,
13089 events: &[crate::engine::ChangeEvent],
13090 ) {
13091 use crate::engine::ChangeEvent;
13092 use formualizer_common::LiteralValue;
13093
13094 #[allow(clippy::type_complexity)]
13095 let rect_from_snapshot =
13096 |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
13097 -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
13098 if snap.target_cells.is_empty() {
13099 return None;
13100 }
13101 let sheet_id = snap.target_cells[0].sheet_id;
13102 let sr0 = snap.target_cells[0].coord.row();
13103 let sc0 = snap.target_cells[0].coord.col();
13104 if snap.values.is_empty() || snap.values[0].is_empty() {
13105 return None;
13106 }
13107 let h = snap.values.len() as u32;
13108 let w = snap.values[0].len() as u32;
13109 let er0 = sr0.saturating_add(h.saturating_sub(1));
13110 let ec0 = sc0.saturating_add(w.saturating_sub(1));
13111 Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
13112 };
13113
13114 for ev in events {
13115 match ev {
13116 ChangeEvent::SpillCommitted { old, new, .. } => {
13117 if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
13118 let old_vals = if let Some(old_snap) = old {
13119 rect_from_snapshot(old_snap)
13120 .map(|(_, _, _, _, _, v)| v)
13121 .unwrap_or_else(|| {
13122 vec![
13123 vec![LiteralValue::Empty; new_vals[0].len()];
13124 new_vals.len()
13125 ]
13126 })
13127 } else {
13128 vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
13129 };
13130 undo.record_restore_computed_rect(
13131 sid, sr0, sc0, er0, ec0, old_vals, new_vals,
13132 );
13133 }
13134 }
13135 ChangeEvent::SpillCleared { old, .. } => {
13136 let width = old.values.first().map_or(0, Vec::len);
13137 if old.target_cells.len() != old.values.len().saturating_mul(width) {
13138 if let Some(first) = old.target_cells.first() {
13139 for cell in &old.target_cells {
13140 let r = (cell.coord.row() - first.coord.row()) as usize;
13141 let c = (cell.coord.col() - first.coord.col()) as usize;
13142 let value = old
13143 .values
13144 .get(r)
13145 .and_then(|row| row.get(c))
13146 .cloned()
13147 .unwrap_or(LiteralValue::Empty);
13148 undo.record_computed_cell(
13149 cell.sheet_id,
13150 cell.coord.row(),
13151 cell.coord.col(),
13152 Some(value),
13153 Some(LiteralValue::Empty),
13154 );
13155 }
13156 }
13157 continue;
13158 }
13159 if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
13160 let new_vals =
13161 vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
13162 undo.record_restore_computed_rect(
13163 sid, sr0, sc0, er0, ec0, old_vals, new_vals,
13164 );
13165 }
13166 }
13167 _ => {}
13168 }
13169 }
13170 }
13171
13172 fn mirror_value_to_computed_overlay(
13177 &mut self,
13178 sheet: &str,
13179 row: u32,
13180 col: u32,
13181 value: &LiteralValue,
13182 ) {
13183 if !(self.config.arrow_storage_enabled
13184 && self.config.delta_overlay_enabled
13185 && self.config.write_formula_overlay_enabled)
13186 {
13187 return;
13188 }
13189 if self.computed_overlay_mirroring_disabled {
13190 return;
13191 }
13192
13193 let date_system = self.arrow_sheet_date_system(sheet);
13194 let ov = Self::literal_to_overlay_value(value, date_system);
13195 self.write_computed_overlay_value_0based(
13196 sheet,
13197 row.saturating_sub(1),
13198 col.saturating_sub(1),
13199 ov,
13200 );
13201 }
13202
13203 fn record_derived_format(&self, vertex_id: VertexId, format: Option<crate::format::FormatId>) {
13204 if let Some(cell) = self.graph.get_cell_ref(vertex_id) {
13205 self.record_derived_format_at(cell, format);
13206 }
13207 }
13208
13209 fn record_derived_format_at(&self, cell: CellRef, format: Option<crate::format::FormatId>) {
13210 #[cfg(test)]
13211 self.derived_format_operations_for_test
13212 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13213 let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
13214 self.derived_formats.set(cell, format);
13215 }
13216
13217 fn clear_cell_format_state(&mut self, sheet: &str, cell: CellRef) {
13218 self.derived_formats.set(cell, None);
13219 if let Some(arrow) = self.arrow_sheets.sheet_mut(sheet) {
13220 arrow.clear_format(cell.coord.row() as usize, cell.coord.col() as usize);
13221 }
13222 }
13223
13224 fn clear_logged_cell_format_states(&mut self, events: &[ChangeEvent]) {
13225 let cells = events
13226 .iter()
13227 .filter_map(|event| match event {
13228 ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
13229 Some(*addr)
13230 }
13231 _ => None,
13232 })
13233 .collect::<FxHashSet<_>>();
13234 for cell in cells {
13235 let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
13236 self.clear_cell_format_state(&sheet, cell);
13237 }
13238 }
13239
13240 fn purge_derived_formats_after_row(&mut self, sheet_id: SheetId, start0: u32) {
13241 self.derived_formats
13242 .retain(|cell| cell.sheet_id != sheet_id || cell.coord.row() < start0);
13243 }
13244
13245 fn purge_derived_formats_after_col(&mut self, sheet_id: SheetId, start0: u32) {
13246 self.derived_formats
13247 .retain(|cell| cell.sheet_id != sheet_id || cell.coord.col() < start0);
13248 }
13249
13250 fn purge_derived_formats_for_sheet(&mut self, sheet_id: SheetId) {
13251 self.derived_formats
13252 .retain(|cell| cell.sheet_id != sheet_id);
13253 }
13254
13255 #[cfg(test)]
13256 pub(crate) fn debug_computed_overlay_format_0based(
13257 &self,
13258 sheet: &str,
13259 row0: u32,
13260 col0: u32,
13261 ) -> Option<crate::format::FormatId> {
13262 let sheet = self.arrow_sheets.sheet(sheet)?;
13263 let (chunk_idx, row_in_chunk) = sheet.chunk_of_row(row0 as usize)?;
13264 sheet
13265 .columns
13266 .get(col0 as usize)?
13267 .chunk(chunk_idx)?
13268 .computed_overlay
13269 .get_format(row_in_chunk)
13270 }
13271
13272 #[cfg(test)]
13273 pub(crate) fn debug_computed_overlay_chunk_has_formats_0based(
13274 &self,
13275 sheet: &str,
13276 row0: u32,
13277 col0: u32,
13278 ) -> bool {
13279 let Some(sheet) = self.arrow_sheets.sheet(sheet) else {
13280 return false;
13281 };
13282 let Some((chunk_idx, _)) = sheet.chunk_of_row(row0 as usize) else {
13283 return false;
13284 };
13285 sheet
13286 .columns
13287 .get(col0 as usize)
13288 .and_then(|column| column.chunk(chunk_idx))
13289 .is_some_and(|chunk| chunk.computed_overlay.has_formats())
13290 }
13291
13292 #[cfg(test)]
13293 pub(crate) fn debug_clear_derived_format_0based(&mut self, sheet: &str, row0: u32, col0: u32) {
13294 if let Some(sheet_id) = self.graph.sheet_id(sheet) {
13295 self.derived_formats
13296 .set(CellRef::new_absolute(sheet_id, row0, col0), None);
13297 }
13298 }
13299
13300 #[cfg(test)]
13301 pub(crate) fn debug_record_derived_format_0based(
13302 &self,
13303 sheet: &str,
13304 row0: u32,
13305 col0: u32,
13306 format: Option<crate::format::FormatId>,
13307 ) {
13308 if let Some(sheet_id) = self.graph.sheet_id(sheet) {
13309 self.record_derived_format_at(CellRef::new_absolute(sheet_id, row0, col0), format);
13310 }
13311 }
13312
13313 #[cfg(test)]
13314 pub(crate) fn debug_derived_format_0based(
13315 &self,
13316 sheet: &str,
13317 row0: u32,
13318 col0: u32,
13319 ) -> Option<crate::format::FormatId> {
13320 let sheet_id = self.graph.sheet_id(sheet)?;
13321 self.derived_formats
13322 .get(&CellRef::new_absolute(sheet_id, row0, col0))
13323 }
13324
13325 #[cfg(test)]
13326 pub(crate) fn debug_reset_format_write_operation_counts(&mut self) {
13327 self.derived_format_operations_for_test
13328 .store(0, std::sync::atomic::Ordering::Relaxed);
13329 self.computed_overlay_set_explicit_entry_operations_for_test = 0;
13330 self.computed_overlay_stale_clear_range_effects_for_test = 0;
13331 self.computed_overlay_stale_clear_offset_attempts_for_test = 0;
13332 self.computed_format_vector_allocations_for_test
13333 .store(0, std::sync::atomic::Ordering::Relaxed);
13334 }
13335
13336 #[cfg(test)]
13337 pub(crate) fn debug_format_write_operation_counts(&self) -> (u64, u64, u64, u64, u64) {
13338 (
13339 self.derived_format_operations_for_test
13340 .load(std::sync::atomic::Ordering::Relaxed),
13341 self.computed_overlay_set_explicit_entry_operations_for_test,
13342 self.computed_format_vector_allocations_for_test
13343 .load(std::sync::atomic::Ordering::Relaxed),
13344 self.computed_overlay_stale_clear_range_effects_for_test,
13345 self.computed_overlay_stale_clear_offset_attempts_for_test,
13346 )
13347 }
13348
13349 fn write_computed_overlay_format_0based(
13350 &mut self,
13351 sheet: &str,
13352 row0: u32,
13353 col0: u32,
13354 format: Option<crate::format::FormatId>,
13355 ) {
13356 self.ensure_arrow_sheet(sheet);
13357 let (row0, col0) = (row0 as usize, col0 as usize);
13358 let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
13359 return;
13360 };
13361 if col0 >= asheet.columns.len() {
13362 asheet.insert_columns(asheet.columns.len(), col0 + 1 - asheet.columns.len());
13363 }
13364 if row0 >= asheet.nrows as usize {
13365 asheet.ensure_row_capacity(row0 + 1);
13366 }
13367 let Some((chunk, offset)) = asheet.chunk_of_row(row0) else {
13368 return;
13369 };
13370 if let Some(chunk) = asheet.ensure_column_chunk_mut(col0, chunk) {
13371 chunk.computed_overlay.set_format(offset, format);
13372 }
13373 }
13374
13375 fn write_computed_cell_0based(
13380 &mut self,
13381 cell: CellRef,
13382 value: &LiteralValue,
13383 format: Option<crate::format::FormatId>,
13384 ) {
13385 if !(self.config.arrow_storage_enabled
13386 && self.config.delta_overlay_enabled
13387 && self.config.write_formula_overlay_enabled)
13388 || self.computed_overlay_mirroring_disabled
13389 {
13390 return;
13391 }
13392 if self.arrow_sheet_by_id(cell.sheet_id).is_none() {
13393 let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
13394 self.ensure_arrow_sheet(&sheet);
13395 }
13396 let (row0, col0) = (cell.coord.row() as usize, cell.coord.col() as usize);
13397 let Some(asheet) = self.arrow_sheet_by_id_mut(cell.sheet_id) else {
13398 return;
13399 };
13400 let ov = Self::literal_to_overlay_value(value, asheet.date_system);
13401 let cur_cols = asheet.columns.len();
13402 if col0 >= cur_cols {
13403 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
13404 }
13405 if row0 >= asheet.nrows as usize {
13406 asheet.ensure_row_capacity(row0 + 1);
13407 }
13408 let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
13409 return;
13410 };
13411 let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
13412 return;
13413 };
13414 let delta = ch.computed_overlay.set_scalar(in_off, ov);
13415 ch.computed_overlay.set_format(in_off, format);
13416 self.adjust_computed_overlay_bytes(delta);
13417 if let Some(cap) = self.config.max_overlay_memory_bytes
13418 && self.computed_overlay_bytes_estimate > cap
13419 {
13420 self.disable_computed_overlay_mirroring_due_to_budget(cap);
13421 }
13422 }
13423
13424 fn write_computed_overlay_value_0based(
13425 &mut self,
13426 sheet: &str,
13427 row0: u32,
13428 col0: u32,
13429 value: OverlayValue,
13430 ) {
13431 if !(self.config.arrow_storage_enabled
13432 && self.config.delta_overlay_enabled
13433 && self.config.write_formula_overlay_enabled)
13434 {
13435 return;
13436 }
13437 if self.computed_overlay_mirroring_disabled {
13438 return;
13439 }
13440
13441 self.ensure_arrow_sheet(sheet);
13442
13443 let row0 = row0 as usize;
13444 let col0 = col0 as usize;
13445 let asheet = self
13446 .arrow_sheets
13447 .sheet_mut(sheet)
13448 .expect("ArrowSheet must exist");
13449
13450 let cur_cols = asheet.columns.len();
13451 if col0 >= cur_cols {
13452 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
13453 }
13454
13455 if row0 >= asheet.nrows as usize {
13456 if asheet.columns.is_empty() {
13457 asheet.insert_columns(0, 1);
13458 }
13459 asheet.ensure_row_capacity(row0 + 1);
13460 }
13461
13462 let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
13463 return;
13464 };
13465 let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
13466 return;
13467 };
13468
13469 let delta = ch.computed_overlay.set_scalar(in_off, value);
13470 self.adjust_computed_overlay_bytes(delta);
13471
13472 if let Some(cap) = self.config.max_overlay_memory_bytes
13473 && self.computed_overlay_bytes_estimate > cap
13474 {
13475 self.disable_computed_overlay_mirroring_due_to_budget(cap);
13476 }
13477 }
13478
13479 pub(crate) fn plan_computed_write_coalescing(
13480 &self,
13481 buffer: &ComputedWriteBuffer,
13482 ) -> ComputedWriteCoalescingPlan {
13483 self.plan_computed_write_coalescing_from_writes(
13484 buffer.writes().iter().cloned(),
13485 buffer.formats_present,
13486 )
13487 }
13488
13489 fn plan_owned_computed_write_coalescing(
13490 &self,
13491 writes: Vec<ComputedWrite>,
13492 formats_present: bool,
13493 ) -> ComputedWriteCoalescingPlan {
13494 self.plan_computed_write_coalescing_from_writes(writes, formats_present)
13495 }
13496
13497 fn plan_computed_write_coalescing_from_writes(
13498 &self,
13499 writes: impl IntoIterator<Item = ComputedWrite>,
13500 formats_present: bool,
13501 ) -> ComputedWriteCoalescingPlan {
13502 let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
13503 BTreeMap::new();
13504 let mut input_cells = 0usize;
13505 let mut located: Option<(SheetId, Option<&crate::arrow_store::ArrowSheet>)> = None;
13507
13508 for write in writes {
13509 match write {
13510 ComputedWrite::Cell {
13511 seq,
13512 sheet_id,
13513 row0,
13514 col0,
13515 value,
13516 format_id,
13517 } => {
13518 input_cells = input_cells.saturating_add(1);
13519 let sheet = match located {
13520 Some((id, sheet)) if id == sheet_id => sheet,
13521 _ => {
13522 let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
13523 located = Some((sheet_id, sheet));
13524 sheet
13525 }
13526 };
13527 let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
13528 Some(sheet) => {
13529 Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize)
13530 }
13531 None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
13532 row0 as usize,
13533 32 * 1024,
13534 ),
13535 };
13536 groups
13537 .entry(ComputedWriteChunkKey {
13538 sheet_id,
13539 col0,
13540 chunk_idx,
13541 chunk_start_row0,
13542 })
13543 .or_default()
13544 .push(ComputedWriteChunkEntryPlan {
13545 row_in_chunk,
13546 seq,
13547 value,
13548 format_id,
13549 });
13550 }
13551 ComputedWrite::Run {
13552 seq,
13553 sheet_id,
13554 row0,
13555 col0,
13556 entries,
13557 } => {
13558 input_cells = input_cells.saturating_add(entries.len());
13559 let sheet = match located {
13560 Some((id, sheet)) if id == sheet_id => sheet,
13561 _ => {
13562 let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
13563 located = Some((sheet_id, sheet));
13564 sheet
13565 }
13566 };
13567 let mut segment: Vec<ComputedWriteChunkEntryPlan> = Vec::new();
13571 let mut segment_key: Option<ComputedWriteChunkKey> = None;
13572 for (k, (value, format_id)) in entries.into_iter().enumerate() {
13573 let row = row0.saturating_add(k as u32) as usize;
13574 let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
13575 Some(sheet) => {
13576 Self::locate_row_in_sheet_for_computed_write_plan(sheet, row)
13577 }
13578 None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
13579 row,
13580 32 * 1024,
13581 ),
13582 };
13583 let key = ComputedWriteChunkKey {
13584 sheet_id,
13585 col0,
13586 chunk_idx,
13587 chunk_start_row0,
13588 };
13589 if segment_key != Some(key)
13590 && let Some(done) = segment_key.replace(key)
13591 {
13592 groups.entry(done).or_default().append(&mut segment);
13593 }
13594 segment.push(ComputedWriteChunkEntryPlan {
13595 row_in_chunk,
13596 seq,
13597 value,
13598 format_id,
13599 });
13600 }
13601 if let Some(done) = segment_key {
13602 groups.entry(done).or_default().append(&mut segment);
13603 }
13604 }
13605 ComputedWrite::Rect {
13606 seq,
13607 sheet_id,
13608 sr0,
13609 sc0,
13610 values,
13611 } => {
13612 for (r_off, row) in values.into_iter().enumerate() {
13613 for (c_off, value) in row.into_iter().enumerate() {
13614 input_cells = input_cells.saturating_add(1);
13615 self.push_computed_write_plan_entry(
13616 &mut groups,
13617 seq,
13618 sheet_id,
13619 sr0.saturating_add(r_off as u32),
13620 sc0.saturating_add(c_off as u32),
13621 value,
13622 None,
13623 );
13624 }
13625 }
13626 }
13627 }
13628 }
13629
13630 let mut plan = ComputedWriteCoalescingPlan {
13631 chunks: Vec::with_capacity(groups.len()),
13632 input_cells,
13633 coalesced_cells: 0,
13634 overwritten_cells: 0,
13635 };
13636 for (key, entries) in groups {
13637 let computed_lane_has_formats =
13638 self.computed_overlay_chunk_has_formats(key.sheet_id, key.col0, key.chunk_idx);
13639 let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(
13640 key,
13641 entries,
13642 formats_present,
13643 computed_lane_has_formats,
13644 );
13645 #[cfg(test)]
13646 if matches!(
13647 &chunk_plan.format_effect,
13648 ComputedWriteChunkFormatEffect::SetExplicit(_)
13649 ) {
13650 self.computed_format_vector_allocations_for_test
13651 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13652 }
13653 plan.coalesced_cells = plan
13654 .coalesced_cells
13655 .saturating_add(chunk_plan.entries.len());
13656 plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
13657 plan.chunks.push(chunk_plan);
13658 }
13659 debug_assert_eq!(
13660 plan.input_cells,
13661 plan.coalesced_cells.saturating_add(plan.overwritten_cells)
13662 );
13663 plan
13664 }
13665
13666 fn push_computed_write_plan_entry(
13667 &self,
13668 groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
13669 seq: u64,
13670 sheet_id: SheetId,
13671 row0: u32,
13672 col0: u32,
13673 value: OverlayValue,
13674 format_id: Option<crate::format::FormatId>,
13675 ) {
13676 let (chunk_idx, chunk_start_row0, row_in_chunk) =
13677 self.locate_computed_write_chunk(sheet_id, row0);
13678 let key = ComputedWriteChunkKey {
13679 sheet_id,
13680 col0,
13681 chunk_idx,
13682 chunk_start_row0,
13683 };
13684 groups
13685 .entry(key)
13686 .or_default()
13687 .push(ComputedWriteChunkEntryPlan {
13688 row_in_chunk,
13689 seq,
13690 value,
13691 format_id,
13692 });
13693 }
13694
13695 fn computed_overlay_chunk_has_formats(
13696 &self,
13697 sheet_id: SheetId,
13698 col0: u32,
13699 chunk_idx: usize,
13700 ) -> bool {
13701 let sheet_name = self.graph.sheet_name(sheet_id);
13702 self.arrow_sheets
13703 .sheet(sheet_name)
13704 .and_then(|sheet| sheet.columns.get(col0 as usize))
13705 .and_then(|column| column.chunk(chunk_idx))
13706 .is_some_and(|chunk| chunk.computed_overlay.has_formats())
13707 }
13708
13709 fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
13710 let sheet_name = self.graph.sheet_name(sheet_id);
13711 if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
13712 return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
13713 }
13714 Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
13715 }
13716
13717 fn locate_row_in_sheet_for_computed_write_plan(
13718 sheet: &crate::arrow_store::ArrowSheet,
13719 row0: usize,
13720 ) -> (usize, u32, usize) {
13721 if row0 < sheet.nrows as usize
13722 && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
13723 {
13724 let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
13725 return (chunk_idx, chunk_start as u32, row_in_chunk);
13726 }
13727
13728 let chunk_rows = sheet.chunk_rows.max(1);
13729 if sheet.chunk_starts.is_empty() {
13730 return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
13731 }
13732
13733 let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
13734 let mut chunk_start = sheet.chunk_starts[chunk_idx];
13735 while chunk_start.saturating_add(chunk_rows) <= row0 {
13736 chunk_idx = chunk_idx.saturating_add(1);
13737 chunk_start = chunk_start.saturating_add(chunk_rows);
13738 }
13739 (
13740 chunk_idx,
13741 chunk_start as u32,
13742 row0.saturating_sub(chunk_start),
13743 )
13744 }
13745
13746 fn locate_row_in_empty_sheet_for_computed_write_plan(
13747 row0: usize,
13748 chunk_rows: usize,
13749 ) -> (usize, u32, usize) {
13750 let chunk_rows = chunk_rows.max(1);
13751 let chunk_idx = row0 / chunk_rows;
13752 let chunk_start = chunk_idx.saturating_mul(chunk_rows);
13753 (
13754 chunk_idx,
13755 chunk_start as u32,
13756 row0.saturating_sub(chunk_start),
13757 )
13758 }
13759
13760 #[cfg(test)]
13761 pub(crate) fn debug_plan_computed_write_coalescing(
13762 &self,
13763 buffer: &ComputedWriteBuffer,
13764 ) -> ComputedWriteCoalescingPlan {
13765 self.plan_computed_write_coalescing(buffer)
13766 }
13767
13768 pub(crate) fn flush_computed_write_buffer(
13769 &mut self,
13770 buffer: &mut ComputedWriteBuffer,
13771 ) -> Result<(), ExcelError> {
13772 if buffer.is_empty() {
13773 return Ok(());
13774 }
13775
13776 self.resource_checkpoint(0)?;
13781 let commit_started = self.preflight_evaluation_commit_window(buffer.len())?;
13782 let (writes, formats_present) = buffer.take_writes();
13783 let mut claims = std::mem::take(&mut buffer.read_claims);
13784 let plan = self.plan_owned_computed_write_coalescing(writes, formats_present);
13785 if !claims.is_empty() {
13786 for chunk in &plan.chunks {
13787 for entry in &chunk.entries {
13788 let key = (
13789 chunk.sheet_id,
13790 chunk
13791 .chunk_start_row0
13792 .saturating_add(entry.row_in_chunk as u32),
13793 chunk.col0,
13794 );
13795 if let Some(claim) = claims.get_mut(&key) {
13796 claim.winning = claim.seq == entry.seq;
13797 }
13798 }
13799 }
13800 }
13801 self.flush_computed_write_plan(plan);
13802 for claim in claims.into_values() {
13803 if claim.winning {
13804 self.freshness_record_installed(&claim.result, true);
13805 }
13806 }
13807 self.observe_evaluation_commit_window(commit_started);
13808
13809 Ok(())
13810 }
13811
13812 fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
13813 for chunk in plan.chunks {
13814 self.flush_computed_write_chunk_plan(chunk);
13815 }
13816 }
13817
13818 fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
13819 match &chunk.shape {
13820 ComputedWriteChunkPlanShape::Point => {
13821 self.flush_computed_write_chunk_plan_as_points(chunk);
13822 }
13823 ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
13824 self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
13825 }
13826 ComputedWriteChunkPlanShape::DenseRange { .. } => {
13827 self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
13828 }
13829 ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
13830 if Self::should_emit_computed_run_fragment(*len, *runs) {
13831 self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
13832 } else {
13833 self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
13834 }
13835 }
13836 }
13837 }
13838
13839 #[inline]
13840 fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
13841 runs <= len / 2
13842 }
13843
13844 fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
13845 let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
13846 for entry in chunk.entries {
13847 let row0 = chunk
13848 .chunk_start_row0
13849 .saturating_add(entry.row_in_chunk as u32);
13850 self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
13851 }
13852 self.apply_computed_overlay_format_effect(
13853 chunk.sheet_id,
13854 chunk.col0,
13855 chunk.chunk_idx,
13856 chunk.format_effect,
13857 );
13858 }
13859
13860 fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
13861 &mut self,
13862 chunk: ComputedWriteChunkPlan,
13863 ) {
13864 let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
13865 let sheet_id = chunk.sheet_id;
13866 let col0 = chunk.col0;
13867 let chunk_idx = chunk.chunk_idx;
13868 let chunk_start_row0 = chunk.chunk_start_row0;
13869 let format_effect = chunk.format_effect;
13870 let items: Vec<(usize, OverlayValue)> = chunk
13871 .entries
13872 .into_iter()
13873 .map(|entry| (entry.row_in_chunk, entry.value))
13874 .collect();
13875 match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
13876 items,
13877 point_estimate,
13878 ) {
13879 Some(Ok(fragment)) => {
13880 self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
13881 }
13882 Some(Err(cells)) => {
13883 self.flush_computed_overlay_cells_as_points(
13884 sheet_id,
13885 col0,
13886 chunk_start_row0,
13887 cells,
13888 );
13889 }
13890 None => {}
13891 }
13892 self.apply_computed_overlay_format_effect(sheet_id, col0, chunk_idx, format_effect);
13893 }
13894
13895 #[inline]
13896 fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
13897 chunk
13898 .entries
13899 .iter()
13900 .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
13901 .fold(0usize, usize::saturating_add)
13902 }
13903
13904 fn flush_computed_overlay_cells_as_points(
13905 &mut self,
13906 sheet_id: SheetId,
13907 col0: u32,
13908 chunk_start_row0: u32,
13909 cells: Vec<(usize, OverlayValue)>,
13910 ) {
13911 let sheet_name = self.graph.sheet_name(sheet_id).to_string();
13912 for (row_in_chunk, value) in cells {
13913 let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
13914 self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
13915 }
13916 }
13917
13918 fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
13919 if chunk.entries.is_empty() {
13920 return;
13921 }
13922 let start = chunk.entries[0].row_in_chunk;
13923 let values: Vec<OverlayValue> =
13924 chunk.entries.into_iter().map(|entry| entry.value).collect();
13925 if let Some(fragment) = OverlayFragment::dense_range(start, values) {
13926 self.apply_computed_overlay_fragment(
13927 chunk.sheet_id,
13928 chunk.col0,
13929 chunk.chunk_idx,
13930 fragment,
13931 );
13932 }
13933 self.apply_computed_overlay_format_effect(
13934 chunk.sheet_id,
13935 chunk.col0,
13936 chunk.chunk_idx,
13937 chunk.format_effect,
13938 );
13939 }
13940
13941 fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
13942 if chunk.entries.is_empty() {
13943 return;
13944 }
13945 let start = chunk.entries[0].row_in_chunk;
13946 let values: Vec<OverlayValue> =
13947 chunk.entries.into_iter().map(|entry| entry.value).collect();
13948 if let Some(fragment) = OverlayFragment::run_range(start, values) {
13949 self.apply_computed_overlay_fragment(
13950 chunk.sheet_id,
13951 chunk.col0,
13952 chunk.chunk_idx,
13953 fragment,
13954 );
13955 }
13956 self.apply_computed_overlay_format_effect(
13957 chunk.sheet_id,
13958 chunk.col0,
13959 chunk.chunk_idx,
13960 chunk.format_effect,
13961 );
13962 }
13963
13964 fn apply_computed_overlay_format_effect(
13965 &mut self,
13966 sheet_id: SheetId,
13967 col0: u32,
13968 chunk_idx: usize,
13969 effect: ComputedWriteChunkFormatEffect,
13970 ) {
13971 if !(self.config.arrow_storage_enabled
13972 && self.config.delta_overlay_enabled
13973 && self.config.write_formula_overlay_enabled)
13974 || self.computed_overlay_mirroring_disabled
13975 {
13976 return;
13977 }
13978
13979 let sheet_name = self.graph.sheet_name(sheet_id);
13980 let Some(sheet) = self.arrow_sheets.sheet_mut(sheet_name) else {
13981 return;
13982 };
13983 let Some(chunk) = sheet
13984 .columns
13985 .get_mut(col0 as usize)
13986 .and_then(|column| column.chunk_mut(chunk_idx))
13987 else {
13988 return;
13989 };
13990 match effect {
13991 ComputedWriteChunkFormatEffect::NoFormatWork => {}
13992 ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Range {
13993 start,
13994 end,
13995 }) => {
13996 #[cfg(test)]
13997 {
13998 self.computed_overlay_stale_clear_range_effects_for_test = self
13999 .computed_overlay_stale_clear_range_effects_for_test
14000 .saturating_add(1);
14001 }
14002 chunk.computed_overlay.clear_format_range(start, end);
14003 }
14004 ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Offsets(
14005 offsets,
14006 )) => {
14007 #[cfg(test)]
14008 {
14009 self.computed_overlay_stale_clear_offset_attempts_for_test = self
14010 .computed_overlay_stale_clear_offset_attempts_for_test
14011 .saturating_add(offsets.len() as u64);
14012 }
14013 chunk.computed_overlay.clear_format_offsets(&offsets);
14014 }
14015 ComputedWriteChunkFormatEffect::SetExplicit(formats) => {
14016 #[cfg(test)]
14017 {
14018 self.computed_overlay_set_explicit_entry_operations_for_test = self
14019 .computed_overlay_set_explicit_entry_operations_for_test
14020 .saturating_add(formats.len() as u64);
14021 }
14022 for (row_in_chunk, format_id) in formats {
14023 chunk.computed_overlay.set_format(row_in_chunk, format_id);
14024 }
14025 }
14026 }
14027 }
14028
14029 fn apply_computed_overlay_fragment(
14030 &mut self,
14031 sheet_id: SheetId,
14032 col0: u32,
14033 chunk_idx: usize,
14034 fragment: OverlayFragment,
14035 ) {
14036 if !(self.config.arrow_storage_enabled
14037 && self.config.delta_overlay_enabled
14038 && self.config.write_formula_overlay_enabled)
14039 {
14040 return;
14041 }
14042 if self.computed_overlay_mirroring_disabled {
14043 return;
14044 }
14045
14046 let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14047 self.ensure_arrow_sheet(&sheet_name);
14048
14049 let col0 = col0 as usize;
14050 let asheet = self
14051 .arrow_sheets
14052 .sheet_mut(&sheet_name)
14053 .expect("ArrowSheet must exist");
14054
14055 let cur_cols = asheet.columns.len();
14056 if col0 >= cur_cols {
14057 asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
14058 }
14059
14060 let start_row0 = asheet
14061 .chunk_starts
14062 .get(chunk_idx)
14063 .copied()
14064 .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
14065 let required_rows =
14066 start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
14067 if required_rows > asheet.nrows as usize {
14068 if asheet.columns.is_empty() {
14069 asheet.insert_columns(0, 1);
14070 }
14071 asheet.ensure_row_capacity(required_rows);
14072 }
14073
14074 let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
14075 return;
14076 };
14077 let delta = ch.computed_overlay.apply_fragment(fragment);
14078 self.adjust_computed_overlay_bytes(delta);
14079
14080 if let Some(cap) = self.config.max_overlay_memory_bytes
14081 && self.computed_overlay_bytes_estimate > cap
14082 {
14083 self.disable_computed_overlay_mirroring_due_to_budget(cap);
14084 }
14085 }
14086
14087 #[inline]
14088 fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
14089 if delta >= 0 {
14090 self.computed_overlay_bytes_estimate = self
14091 .computed_overlay_bytes_estimate
14092 .saturating_add(delta as usize);
14093 } else {
14094 self.computed_overlay_bytes_estimate = self
14095 .computed_overlay_bytes_estimate
14096 .saturating_sub((-delta) as usize);
14097 }
14098 }
14099
14100 fn clear_all_computed_overlays(&mut self) {
14101 let mut freed_total = 0usize;
14102 for sh in self.arrow_sheets.sheets.iter_mut() {
14103 for col in sh.columns.iter_mut() {
14104 for ch in col.chunks.iter_mut() {
14105 freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
14106 }
14107 for ch in col.sparse_chunks.values_mut() {
14108 freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
14109 }
14110 }
14111 }
14112 self.computed_overlay_bytes_estimate = self
14113 .computed_overlay_bytes_estimate
14114 .saturating_sub(freed_total);
14115 }
14116
14117 fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
14118 self.compact_all_computed_overlays();
14121 }
14122
14123 fn compact_all_computed_overlays(&mut self) {
14126 let mut freed_total = 0usize;
14127 for sheet in self.arrow_sheets.sheets.iter_mut() {
14128 for col_idx in 0..sheet.columns.len() {
14129 let num_dense = sheet.columns[col_idx].chunks.len();
14131 for ch_idx in 0..num_dense {
14132 freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
14133 }
14134 let sparse_keys: Vec<usize> = sheet.columns[col_idx]
14136 .sparse_chunks
14137 .keys()
14138 .copied()
14139 .collect();
14140 for ch_idx in sparse_keys {
14141 freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
14142 }
14143 }
14144 }
14145 self.computed_overlay_bytes_estimate = self
14146 .computed_overlay_bytes_estimate
14147 .saturating_sub(freed_total);
14148 self.overlay_compactions = self.overlay_compactions.saturating_add(1);
14149 }
14150
14151 fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
14152 let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
14153 }
14154
14155 fn record_vertex_value_to_overlay(
14156 &mut self,
14157 vertex_id: VertexId,
14158 value: &LiteralValue,
14159 computed_writes: Option<&mut ComputedWriteBuffer>,
14160 ) -> Result<OverlayWriteDisposition, ExcelError> {
14161 if !(self.config.arrow_storage_enabled
14162 && self.config.delta_overlay_enabled
14163 && self.config.write_formula_overlay_enabled)
14164 {
14165 return Ok(OverlayWriteDisposition::Skipped);
14166 }
14167 if self.computed_overlay_mirroring_disabled {
14168 return Ok(OverlayWriteDisposition::Skipped);
14169 }
14170 if !matches!(
14171 self.graph.get_vertex_kind(vertex_id),
14172 VertexKind::FormulaScalar | VertexKind::FormulaArray
14173 ) {
14174 return Ok(OverlayWriteDisposition::Skipped);
14175 }
14176 let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
14177 return Ok(OverlayWriteDisposition::Skipped);
14178 };
14179 let Some(buffer) = computed_writes else {
14180 let format_id = self.derived_formats.get(&cell);
14182 self.write_computed_cell_0based(cell, value, format_id);
14183 return Ok(OverlayWriteDisposition::Installed);
14184 };
14185 let date_system = self
14186 .arrow_sheet_by_id(cell.sheet_id)
14187 .map(|sheet| sheet.date_system)
14188 .unwrap_or(self.config.date_system);
14189 let ov = Self::literal_to_overlay_value(value, date_system);
14190 {
14191 let format_id = self.derived_formats.get(&cell);
14192 buffer.push_cell_with_format(
14193 cell.sheet_id,
14194 cell.coord.row(),
14195 cell.coord.col(),
14196 ov,
14197 format_id,
14198 );
14199 if self.should_flush_computed_write_buffer(buffer) {
14200 self.flush_computed_write_buffer(buffer)?;
14201 return Ok(OverlayWriteDisposition::Installed);
14202 }
14203 }
14204 Ok(OverlayWriteDisposition::Buffered)
14205 }
14206
14207 #[inline]
14208 fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
14209 self.config.max_overlay_memory_bytes.is_some_and(|cap| {
14210 if cap == 0 {
14211 return false;
14212 }
14213 self.computed_overlay_bytes_estimate
14214 .saturating_add(buffer.estimated_bytes())
14215 > cap
14216 })
14217 }
14218
14219 pub fn overlay_memory_usage(&self) -> usize {
14221 self.computed_overlay_bytes_estimate
14222 }
14223
14224 #[cfg(test)]
14225 pub(crate) fn debug_overlay_compactions(&self) -> u64 {
14226 self.overlay_compactions
14227 }
14228
14229 #[cfg(test)]
14230 pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
14231 let mut total = 0usize;
14232 for sheet in &self.arrow_sheets.sheets {
14233 for column in &sheet.columns {
14234 for chunk in &column.chunks {
14235 total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
14236 }
14237 for chunk in column.sparse_chunks.values() {
14238 total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
14239 }
14240 }
14241 }
14242 self.computed_overlay_bytes_estimate = total;
14243 total
14244 }
14245
14246 fn resolve_sheet_locator_for_write(
14247 &mut self,
14248 loc: formualizer_common::SheetLocator<'_>,
14249 current_sheet: &str,
14250 ) -> Result<SheetId, ExcelError> {
14251 Ok(match loc {
14252 formualizer_common::SheetLocator::Id(id) => id,
14253 formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
14254 formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
14255 })
14256 }
14257
14258 fn resolve_sheet_locator_for_read(
14259 &self,
14260 loc: formualizer_common::SheetLocator<'_>,
14261 current_sheet: &str,
14262 ) -> Result<SheetId, ExcelError> {
14263 match loc {
14264 formualizer_common::SheetLocator::Id(id) => Ok(id),
14265 formualizer_common::SheetLocator::Name(name) => self
14266 .graph
14267 .sheet_id(name.as_ref())
14268 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
14269 formualizer_common::SheetLocator::Current => self
14270 .graph
14271 .sheet_id(current_sheet)
14272 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
14273 }
14274 }
14275
14276 pub fn set_cell_value(
14278 &mut self,
14279 sheet: &str,
14280 row: u32,
14281 col: u32,
14282 value: LiteralValue,
14283 ) -> Result<(), ExcelError> {
14284 self.ensure_not_poisoned()?;
14285 self.observe_function_semantic_epoch()?;
14286 let sheet_existed = self.graph.sheet_id(sheet).is_some();
14287 let sheet_id = self.graph.sheet_id_mut(sheet);
14288 let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14289 let replaced = self
14290 .graph
14291 .get_vertex_id_for_address(&cell_ref)
14292 .filter(|&vertex| {
14293 matches!(
14294 self.graph.get_vertex_kind(vertex),
14295 VertexKind::FormulaScalar | VertexKind::FormulaArray
14296 )
14297 });
14298 let replaced_formula = replaced.is_some();
14299 if let Some(anchor) = replaced
14303 && self.graph.spill_registry_has_anchor(anchor)
14304 {
14305 self.clear_spill_projection_and_mirror(anchor, None);
14306 }
14307 self.graph.set_cell_value(sheet, row, col, value.clone())?;
14308 if value != LiteralValue::Empty {
14309 for table in self.graph.empty_tables_at(cell_ref) {
14310 self.graph.resize_table(&table.name, table.range)?;
14311 self.mark_topology_edited();
14312 }
14313 }
14314 self.clear_cell_format_state(sheet, cell_ref);
14315 self.record_changed_cell(sheet, row, col);
14316 if !sheet_existed || replaced_formula {
14317 self.mark_topology_edited();
14318 }
14319 self.mirror_value_to_overlay(sheet, row, col, &value);
14321 self.mark_data_edited();
14323 Ok(())
14324 }
14325
14326 fn record_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
14328 let sheet_id = self.graph.sheet_id_mut(sheet);
14329 self.record_structural_change(StructuralScope::Cell {
14330 sheet: sheet_id,
14331 row: row.saturating_sub(1),
14332 col: col.saturating_sub(1),
14333 });
14334 }
14335
14336 fn record_inverse_change_for_event(&mut self, event: &ChangeEvent) {
14337 if let ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } = event {
14338 self.record_structural_change(StructuralScope::Region(Region::point(
14341 addr.sheet_id,
14342 addr.coord.row(),
14343 addr.coord.col(),
14344 )));
14345 } else {
14346 self.record_change_for_event(event);
14347 }
14348 }
14349
14350 fn record_change_for_event(&mut self, event: &ChangeEvent) {
14351 match event {
14352 ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
14353 self.record_structural_change(StructuralScope::Cell {
14354 sheet: addr.sheet_id,
14355 row: addr.coord.row(),
14356 col: addr.coord.col(),
14357 });
14358 }
14359 ChangeEvent::SpillCommitted { new, .. } => {
14360 if let Some(scope) = Self::structural_scope_from_cells(&new.target_cells) {
14361 self.record_structural_change(scope);
14362 }
14363 }
14364 ChangeEvent::SpillCleared { anchor, old } => {
14365 if self.graph.is_live_formula_vertex(*anchor) {
14366 self.graph.mark_dirty_many(&[*anchor]);
14367 }
14368 if let Some(scope) = Self::structural_scope_from_cells(&old.target_cells) {
14369 self.record_structural_change(scope);
14370 }
14371 }
14372 ChangeEvent::TableResized { .. }
14373 | ChangeEvent::DefineName { .. }
14374 | ChangeEvent::UpdateName { .. }
14375 | ChangeEvent::DeleteName { .. }
14376 | ChangeEvent::NamedRangeAdjusted { .. } => {
14377 self.record_structural_change(StructuralScope::AllSheets);
14381 }
14382 ChangeEvent::VertexMoved { id, .. } | ChangeEvent::FormulaAdjusted { id, .. } => {
14383 if self.graph.is_live_formula_vertex(*id) {
14387 self.graph.mark_dirty_many(&[*id]);
14388 }
14389 }
14390 ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
14391 self.record_structural_change(StructuralScope::Region(Region::whole_row(
14392 *sheet_id, *row0,
14393 )));
14394 }
14395 ChangeEvent::AddVertex { .. }
14396 | ChangeEvent::RemoveVertex { .. }
14397 | ChangeEvent::EdgeAdded { .. }
14398 | ChangeEvent::EdgeRemoved { .. }
14399 | ChangeEvent::CompoundStart { .. }
14400 | ChangeEvent::CompoundEnd { .. }
14401 | ChangeEvent::StagedFormulaCellChanged { .. } => {}
14402 }
14403 }
14404
14405 fn record_structural_change(&mut self, scope: StructuralScope) {
14406 if let StructuralScope::Cell { sheet, row, col } = scope {
14407 self.wake_spill_owner_of_entered_cell(sheet, row, col);
14408 }
14409 self.wake_spill_blocker_waiters(scope);
14410 self.invalidate_pending_spills(scope);
14411 }
14412
14413 fn wake_spill_blocker_waiters(&mut self, scope: StructuralScope) {
14417 if self.spill_blocker_waiters.is_empty() {
14418 return;
14419 }
14420 let woken = self.spill_blocker_waiters.take_affected(scope);
14421 let woken: Vec<VertexId> = woken
14422 .into_iter()
14423 .filter(|&vertex| {
14424 self.graph.vertex_exists(vertex)
14425 && matches!(
14426 self.graph.get_vertex_kind(vertex),
14427 VertexKind::FormulaScalar | VertexKind::FormulaArray
14428 )
14429 })
14430 .collect();
14431 if !woken.is_empty() {
14432 self.graph.mark_dirty_many(&woken);
14433 }
14434 }
14435
14436 fn wake_spill_owner_of_entered_cell(&mut self, sheet: SheetId, row: u32, col: u32) {
14439 let cell = self.graph.make_cell_ref_internal(sheet, row, col);
14440 if let Some(owner) = self.graph.note_spill_child_edit(cell) {
14441 self.graph.mark_dirty_many(&[owner]);
14442 }
14443 }
14444
14445 fn structural_scope_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
14446 let first = cells.first()?;
14447 let sheet_id = first.sheet_id;
14448 if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
14449 return Some(StructuralScope::OpaqueGlobal);
14450 }
14451 let mut row_start = first.coord.row();
14452 let mut row_end = row_start;
14453 let mut col_start = first.coord.col();
14454 let mut col_end = col_start;
14455 for cell in cells.iter().skip(1) {
14456 row_start = row_start.min(cell.coord.row());
14457 row_end = row_end.max(cell.coord.row());
14458 col_start = col_start.min(cell.coord.col());
14459 col_end = col_end.max(cell.coord.col());
14460 }
14461 Some(StructuralScope::Region(Region::rect(
14462 sheet_id, row_start, row_end, col_start, col_end,
14463 )))
14464 }
14465
14466 pub fn set_cell_value_ref(
14467 &mut self,
14468 cell: formualizer_common::SheetCellRef<'_>,
14469 current_sheet: &str,
14470 value: LiteralValue,
14471 ) -> Result<(), ExcelError> {
14472 self.ensure_not_poisoned()?;
14473 let owned = cell.into_owned();
14474 let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
14475 let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14476 self.set_cell_value(
14477 &sheet_name,
14478 owned.coord.row() + 1,
14479 owned.coord.col() + 1,
14480 value,
14481 )
14482 }
14483
14484 pub fn set_cell_formula_ref(
14485 &mut self,
14486 cell: formualizer_common::SheetCellRef<'_>,
14487 current_sheet: &str,
14488 ast: ASTNode,
14489 ) -> Result<(), ExcelError> {
14490 self.ensure_not_poisoned()?;
14491 let owned = cell.into_owned();
14492 let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
14493 let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14494 self.set_cell_formula(
14495 &sheet_name,
14496 owned.coord.row() + 1,
14497 owned.coord.col() + 1,
14498 ast,
14499 )
14500 }
14501
14502 pub fn get_cell_value_ref(
14503 &self,
14504 cell: formualizer_common::SheetCellRef<'_>,
14505 current_sheet: &str,
14506 ) -> Result<Option<LiteralValue>, ExcelError> {
14507 let owned = cell.into_owned();
14508 let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
14509 let sheet_name = self.graph.sheet_name(sheet_id);
14510 Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
14511 }
14512
14513 pub fn resolve_range_view_sheet_ref<'c>(
14514 &'c self,
14515 r: &formualizer_common::SheetRef<'_>,
14516 current_sheet: &str,
14517 ) -> Result<RangeView<'c>, ExcelError> {
14518 use formualizer_common::SheetLocator;
14519
14520 let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
14521 match loc {
14522 SheetLocator::Current => Ok(None),
14523 SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
14524 SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
14525 }
14526 };
14527
14528 let rt = match r {
14529 formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
14530 sheet: sheet_to_opt_name(cell.sheet.clone())?,
14531 row: cell.coord.row() + 1,
14532 col: cell.coord.col() + 1,
14533 row_abs: cell.coord.row_abs(),
14534 col_abs: cell.coord.col_abs(),
14535 },
14536 formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
14537 sheet: sheet_to_opt_name(range.sheet.clone())?,
14538 start_row: range.start_row.map(|b| b.index + 1),
14539 start_col: range.start_col.map(|b| b.index + 1),
14540 end_row: range.end_row.map(|b| b.index + 1),
14541 end_col: range.end_col.map(|b| b.index + 1),
14542 start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
14543 start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
14544 end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
14545 end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
14546 },
14547 };
14548
14549 crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
14550 }
14551
14552 pub fn set_cell_formula(
14554 &mut self,
14555 sheet: &str,
14556 row: u32,
14557 col: u32,
14558 ast: ASTNode,
14559 ) -> Result<(), ExcelError> {
14560 self.ensure_not_poisoned()?;
14561 self.observe_function_semantic_epoch()?;
14562 let sheet_id = self.graph.sheet_id_mut(sheet);
14563 let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14564 let ingested = {
14565 let mut pipeline = self.ingest_pipeline();
14566 pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
14567 };
14568 self.graph.set_cell_formula_with_plan(
14569 sheet,
14570 row,
14571 col,
14572 ingested.ast_id,
14573 &ingested.dep_plan,
14574 ingested.dep_plan.volatile,
14575 ingested.dep_plan.dynamic,
14576 )?;
14577 for table in self.graph.empty_tables_at(placement) {
14578 self.graph.resize_table(&table.name, table.range)?;
14579 }
14580 self.clear_cell_format_state(sheet, placement);
14581 self.record_changed_cell(sheet, row, col);
14582
14583 self.clear_delta_overlay_cell(sheet, row, col);
14588
14589 self.mark_topology_edited();
14591 Ok(())
14592 }
14593
14594 pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
14596 where
14597 I: IntoIterator<Item = (u32, u32, ASTNode)>,
14598 {
14599 self.ensure_not_poisoned()?;
14600 let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
14601 let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
14602 let sheet_id = self.graph.sheet_id_mut(sheet);
14603 let ingested = {
14604 let mut pipeline = self.ingest_pipeline();
14605 let inputs = collected.into_iter().map(|(row, col, ast)| {
14606 let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14607 (FormulaAstInput::Tree(ast), placement, None)
14608 });
14609 pipeline.ingest_batch(inputs)?
14610 };
14611 let planned: Vec<(u32, u32, AstNodeId, DependencyPlanRow)> = ingested
14612 .into_iter()
14613 .map(|formula| {
14614 (
14615 formula.placement.coord.row() + 1,
14616 formula.placement.coord.col() + 1,
14617 formula.ast_id,
14618 formula.dep_plan,
14619 )
14620 })
14621 .collect();
14622 let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
14623 for (row, col) in edited_cells {
14624 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14625 for table in self.graph.empty_tables_at(cell) {
14626 self.graph.resize_table(&table.name, table.range)?;
14627 }
14628 self.clear_cell_format_state(sheet, cell);
14629 self.record_changed_cell(sheet, row, col);
14630 }
14631 if n > 0 {
14633 self.mark_topology_edited();
14634 }
14635 Ok(n)
14636 }
14637
14638 pub fn declare_dynamic_array_anchor(
14667 &mut self,
14668 sheet: &str,
14669 row: u32,
14670 col: u32,
14671 ) -> Result<(), ExcelError> {
14672 self.ensure_not_poisoned()?;
14673 let not_a_formula = || {
14674 ExcelError::new(ExcelErrorKind::Ref)
14675 .with_message("Declared dynamic array anchor must be a formula cell")
14676 };
14677 if row == 0 || col == 0 {
14678 return Err(not_a_formula());
14679 }
14680 if self.get_staged_formula_text(sheet, row, col).is_some() {
14681 return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
14682 "Declared dynamic array anchor is still staged; build the graph first",
14683 ));
14684 }
14685 let sheet_id = self.graph.sheet_id(sheet).ok_or_else(not_a_formula)?;
14686 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14687 let vertex = self
14688 .graph
14689 .cell_vertex_mut(&cell)
14690 .filter(|v| self.graph.vertex_has_formula(*v))
14691 .ok_or_else(not_a_formula)?;
14692 if self.graph.declare_dynamic_anchor(vertex, cell) {
14693 self.graph.mark_dirty_many(&[vertex]);
14694 self.mark_topology_edited();
14695 }
14696 Ok(())
14697 }
14698
14699 pub fn declare_fixed_array_formula(
14705 &mut self,
14706 sheet: &str,
14707 row: u32,
14708 col: u32,
14709 rows: u32,
14710 cols: u32,
14711 ) -> Result<(), ExcelError> {
14712 self.ensure_not_poisoned()?;
14713 if self.config.family_execution {
14714 return Err(ExcelError::new(ExcelErrorKind::Ref)
14715 .with_message("Fixed arrays require family_execution = false"));
14716 }
14717 if row == 0
14718 || col == 0
14719 || rows == 0
14720 || cols == 0
14721 || row.checked_add(rows - 1).is_none_or(|r| r > 1_048_576)
14722 || col.checked_add(cols - 1).is_none_or(|c| c > 16_384)
14723 || u64::from(rows) * u64::from(cols) > u64::from(self.config.spill.max_spill_cells)
14724 {
14725 return Err(ExcelError::new(ExcelErrorKind::Ref)
14726 .with_message("Invalid or over-cap fixed array extent"));
14727 }
14728 if self.get_staged_formula_text(sheet, row, col).is_some() {
14729 return Err(ExcelError::new(ExcelErrorKind::Ref)
14730 .with_message("Fixed array formula is still staged; build the graph first"));
14731 }
14732 let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
14733 ExcelError::new(ExcelErrorKind::Ref).with_message("Unknown fixed array sheet")
14734 })?;
14735 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14736 let vertex = self
14737 .graph
14738 .cell_vertex_mut(&cell)
14739 .filter(|v| self.graph.vertex_has_formula(*v))
14740 .ok_or_else(|| {
14741 ExcelError::new(ExcelErrorKind::Ref)
14742 .with_message("Fixed array anchor must be a formula cell")
14743 })?;
14744 if (rows == 1 && cols == 1 && self.graph.fixed_single_arrays.contains(&vertex))
14745 || self.graph.fixed_array_shapes.get(&vertex) == Some(&(rows, cols))
14746 {
14747 return Ok(());
14748 }
14749 if self.graph.fixed_single_arrays.contains(&vertex)
14750 || self.graph.spill_registry_has_anchor(vertex)
14751 || self.graph.is_current_declared_dynamic_anchor(vertex)
14752 {
14753 return Err(ExcelError::new(ExcelErrorKind::Ref)
14754 .with_message("Array declaration already exists; replace the formula first"));
14755 }
14756 if rows == 1 && cols == 1 {
14757 self.graph.fixed_single_arrays.insert(vertex);
14758 } else {
14759 let mut targets = Vec::with_capacity((rows * cols) as usize);
14760 for r in 0..rows {
14761 for c in 0..cols {
14762 targets.push(self.graph.make_cell_ref(sheet, row + r, col + c));
14763 }
14764 }
14765 self.graph.plan_spill_region(vertex, &targets)?;
14766 self.graph
14768 .commit_spill_region_atomic_with_fault(vertex, targets, vec![], None)?;
14769 self.graph.fixed_array_shapes.insert(vertex, (rows, cols));
14770 }
14771 self.graph.mark_dirty_many(&[vertex]);
14772 self.mark_topology_edited();
14773 Ok(())
14774 }
14775
14776 #[inline]
14777 fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
14778 match v {
14779 LiteralValue::Empty => None,
14780 LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
14781 other => Some(other),
14782 }
14783 }
14784
14785 fn materialize_temporal_egress(
14786 value: LiteralValue,
14787 class: Option<&formualizer_common::numfmt::FormatClass>,
14788 policy: crate::engine::TemporalEgress,
14789 date_system: crate::engine::DateSystem,
14790 ) -> LiteralValue {
14791 use formualizer_common::numfmt::FormatClass;
14792 if policy == crate::engine::TemporalEgress::Serial {
14793 return value;
14794 }
14795 let LiteralValue::Number(serial) = value else {
14796 return value;
14797 };
14798 match class {
14799 Some(FormatClass::Date) if serial.fract() != 0.0 => {
14800 formualizer_common::try_serial_to_datetime_for(date_system, serial)
14801 .map(LiteralValue::DateTime)
14802 .unwrap_or(LiteralValue::Number(serial))
14803 }
14804 Some(FormatClass::Date) => {
14805 formualizer_common::try_serial_to_date_for(date_system, serial)
14806 .map(LiteralValue::Date)
14807 .unwrap_or(LiteralValue::Number(serial))
14808 }
14809 Some(FormatClass::DateTime) => {
14810 formualizer_common::try_serial_to_datetime_for(date_system, serial)
14811 .map(LiteralValue::DateTime)
14812 .unwrap_or(LiteralValue::Number(serial))
14813 }
14814 Some(FormatClass::Time) if (0.0..1.0).contains(&serial) => {
14815 let nanos = (serial * 86_400_000_000_000.0).round() as u64;
14816 let nanos = nanos.min(86_400_000_000_000 - 1);
14817 chrono::NaiveTime::from_num_seconds_from_midnight_opt(
14818 (nanos / 1_000_000_000) as u32,
14819 (nanos % 1_000_000_000) as u32,
14820 )
14821 .map(LiteralValue::Time)
14822 .unwrap_or(LiteralValue::Number(serial))
14823 }
14824 Some(FormatClass::Time | FormatClass::Duration) => {
14825 let nanos = (serial * 86_400.0 * 1_000_000_000.0).round();
14826 if nanos.is_finite() && nanos >= i64::MIN as f64 && nanos <= i64::MAX as f64 {
14827 LiteralValue::Duration(chrono::Duration::nanoseconds(nanos as i64))
14828 } else {
14829 LiteralValue::Number(serial)
14830 }
14831 }
14832 _ => LiteralValue::Number(serial),
14833 }
14834 }
14835
14836 pub(crate) fn effective_format_id(
14837 &self,
14838 sheet: &str,
14839 row: u32,
14840 col: u32,
14841 ) -> Option<crate::format::FormatId> {
14842 let arrow = self.arrow_sheets.sheet(sheet).and_then(|arrow| {
14843 arrow.format_id(
14844 row.saturating_sub(1) as usize,
14845 col.saturating_sub(1) as usize,
14846 )
14847 });
14848 arrow.or_else(|| {
14849 let sheet_id = self.graph.sheet_id(sheet)?;
14850 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14851 self.derived_formats.get(&cell)
14852 })
14853 }
14854
14855 pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
14857 let raw = self.read_cell_value(sheet, row, col)?;
14858 let format = self.effective_format_id(sheet, row, col);
14859 let class = format.and_then(|id| self.format_registry.class(id));
14860 Self::normalize_public_cell_read(Self::materialize_temporal_egress(
14861 raw,
14862 class,
14863 self.config.temporal_egress,
14864 self.config.date_system,
14865 ))
14866 }
14867
14868 pub fn get_range_values(
14870 &self,
14871 sheet: &str,
14872 sr: u32,
14873 sc: u32,
14874 er: u32,
14875 ec: u32,
14876 ) -> Vec<Vec<LiteralValue>> {
14877 let height = er.saturating_sub(sr).saturating_add(1) as usize;
14878 let width = ec.saturating_sub(sc).saturating_add(1) as usize;
14879 let Some(asheet) = self.sheet_store().sheet(sheet) else {
14880 return vec![vec![LiteralValue::Empty; width]; height];
14881 };
14882 let view = asheet.range_view(
14883 sr.saturating_sub(1) as usize,
14884 sc.saturating_sub(1) as usize,
14885 er.saturating_sub(1) as usize,
14886 ec.saturating_sub(1) as usize,
14887 );
14888 let sheet_id = self.graph.sheet_id(sheet);
14889 let derived_formats = &self.derived_formats;
14890 let has_derived_formats =
14891 sheet_id.is_some_and(|sheet_id| derived_formats.any(|cell| cell.sheet_id == sheet_id));
14892 let mut out = Vec::with_capacity(height);
14893 if !asheet.has_formats() && !has_derived_formats {
14894 for rr in 0..height {
14895 let mut row = Vec::with_capacity(width);
14896 for cc in 0..width {
14897 row.push(view.get_cell(rr, cc));
14898 }
14899 out.push(row);
14900 }
14901 return out;
14902 }
14903 let format_registry = &self.format_registry;
14904 for rr in 0..height {
14905 let mut row = Vec::with_capacity(width);
14906 for cc in 0..width {
14907 let raw = view.get_cell(rr, cc);
14908 let row0 = sr.saturating_sub(1).saturating_add(rr as u32);
14909 let col0 = sc.saturating_sub(1).saturating_add(cc as u32);
14910 let format = asheet.format_id(row0 as usize, col0 as usize).or_else(|| {
14911 let cell = CellRef::new(sheet_id?, Coord::new(row0, col0, true, true));
14912 derived_formats.get(&cell)
14913 });
14914 let class = format.and_then(|id| format_registry.class(id));
14915 row.push(Self::materialize_temporal_egress(
14916 raw,
14917 class,
14918 self.config.temporal_egress,
14919 self.config.date_system,
14920 ));
14921 }
14922 out.push(row);
14923 }
14924 out
14925 }
14926
14927 fn declared_anchor_holds_scalar(&self, anchor: CellRef) -> bool {
14933 let sheet = self.graph.sheet_name(anchor.sheet_id);
14934 if sheet.is_empty() {
14935 return false;
14936 }
14937 matches!(
14938 self.read_cell_value(sheet, anchor.coord.row() + 1, anchor.coord.col() + 1),
14939 Some(value) if !matches!(
14940 value,
14941 LiteralValue::Error(_)
14942 | LiteralValue::Array(_)
14943 | LiteralValue::Pending
14944 | LiteralValue::Empty
14945 )
14946 )
14947 }
14948
14949 pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
14950 let asheet = self.sheet_store().sheet(sheet)?;
14951 let r0 = row.saturating_sub(1) as usize;
14952 let c0 = col.saturating_sub(1) as usize;
14953 let v = asheet.get_cell_value(r0, c0);
14954 if matches!(v, LiteralValue::Empty) {
14955 None
14956 } else {
14957 Some(v)
14958 }
14959 }
14960
14961 pub(crate) fn read_range_values(
14963 &self,
14964 sheet: &str,
14965 sr: u32,
14966 sc: u32,
14967 er: u32,
14968 ec: u32,
14969 ) -> RangeView<'_> {
14970 let Some(asheet) = self.sheet_store().sheet(sheet) else {
14971 return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
14972 };
14973 if er < sr || ec < sc {
14974 return asheet.range_view(1, 1, 0, 0);
14975 }
14976 let sr0 = sr.saturating_sub(1) as usize;
14977 let sc0 = sc.saturating_sub(1) as usize;
14978 let er0 = er.saturating_sub(1) as usize;
14979 let ec0 = ec.saturating_sub(1) as usize;
14980 asheet.range_view(sr0, sc0, er0, ec0)
14981 }
14982
14983 pub fn get_cell(
14985 &self,
14986 sheet: &str,
14987 row: u32,
14988 col: u32,
14989 ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
14990 let v = self.get_cell_value(sheet, row, col);
14991 let sheet_id = self.graph.sheet_id(sheet)?;
14992 let coord = Coord::from_excel(row, col, true, true);
14993 let cell = CellRef::new(sheet_id, coord);
14994 if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
14995 let ast = self.graph.get_formula(vid);
14996 Some((ast, v))
14997 } else if v.is_some() || self.graph.had_legacy_cell_vertex(&cell) {
14998 Some((None, v))
15002 } else {
15003 None
15004 }
15005 }
15006
15007 pub fn begin_batch(&mut self) {
15012 self.assert_not_poisoned();
15013 self.graph.begin_batch();
15014 }
15015
15016 pub fn end_batch(&mut self) {
15021 self.assert_not_poisoned();
15022 self.graph.end_batch();
15023 }
15024
15025 pub fn begin_deferred_dirty(&mut self) {
15037 self.assert_not_poisoned();
15038 self.graph.begin_deferred_dirty();
15039 }
15040
15041 pub fn end_deferred_dirty(&mut self) {
15047 self.assert_not_poisoned();
15048 let _ = self.graph.end_deferred_dirty();
15049 }
15050
15051 pub fn dirty_propagation_visits(&self) -> u64 {
15055 self.graph.dirty_propagation_visits()
15056 }
15057
15058 #[inline]
15061 fn record_cell_if_changed(
15062 delta: &mut DeltaCollector,
15063 cell: &CellRef,
15064 old: &LiteralValue,
15065 new: &LiteralValue,
15066 ) {
15067 if old != new {
15068 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
15069 }
15070 }
15071
15072 pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
15073 self.ensure_not_poisoned()?;
15074 self.observe_evaluation_resource_request(EvaluationRequestKind::Vertex, |engine| {
15075 engine.observe_function_semantic_epoch()?;
15076 engine.resource_checkpoint(1)?;
15078 if !engine.graph.vertex_exists(vertex_id) {
15079 return engine.evaluate_vertex_impl(vertex_id, None);
15080 }
15081 let is_formula = matches!(
15082 engine.graph.get_vertex_kind(vertex_id),
15083 VertexKind::FormulaScalar | VertexKind::FormulaArray
15084 );
15085 if is_formula {
15086 engine.begin_evaluation_request();
15087 #[cfg(any(test, feature = "legacy_oracle"))]
15088 engine.graph.flush_pending_edge_deltas();
15089 let roots = [crate::engine::target_preparation::TargetProducer::Legacy(
15090 vertex_id,
15091 )];
15092 engine.evaluate_legacy_target_roots(&roots, None)?;
15093 }
15094 if !is_formula {
15095 engine.refresh_resolution_snapshots();
15096 }
15097 engine.evaluate_vertex_impl(vertex_id, None)
15098 })
15099 }
15100
15101 fn publish_oversized_spill(
15103 &mut self,
15104 vertex_id: VertexId,
15105 error: ExcelError,
15106 mut delta: Option<&mut DeltaCollector>,
15107 ) -> LiteralValue {
15108 self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
15109 let anchor = self
15110 .graph
15111 .get_cell_ref(vertex_id)
15112 .expect("cell ref for vertex");
15113 let spill_val = LiteralValue::Error(error);
15114 if let Some(d) = delta {
15115 let old = self
15116 .read_cell_value(
15117 self.graph.sheet_name(anchor.sheet_id),
15118 anchor.coord.row() + 1,
15119 anchor.coord.col() + 1,
15120 )
15121 .unwrap_or(LiteralValue::Empty);
15122 if old != spill_val {
15123 d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
15124 }
15125 }
15126 self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15127 if self.config.arrow_storage_enabled
15128 && self.config.delta_overlay_enabled
15129 && self.config.write_formula_overlay_enabled
15130 {
15131 let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15132 self.mirror_value_to_computed_overlay(
15133 &sheet_name,
15134 anchor.coord.row() + 1,
15135 anchor.coord.col() + 1,
15136 &spill_val,
15137 );
15138 }
15139 spill_val
15140 }
15141
15142 fn evaluate_vertex_impl(
15143 &mut self,
15144 vertex_id: VertexId,
15145 delta: Option<&mut DeltaCollector>,
15146 ) -> Result<LiteralValue, ExcelError> {
15147 if !self.graph.vertex_exists(vertex_id) {
15151 return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
15152 .with_message(format!("Vertex not found: {vertex_id:?}")));
15153 }
15154 if (self.active_resource_ledger.is_some()
15155 || self.graph.fixed_array_shapes.contains_key(&vertex_id))
15156 && matches!(
15157 self.graph.get_vertex_kind(vertex_id),
15158 VertexKind::FormulaScalar | VertexKind::FormulaArray
15159 )
15160 {
15161 let value = self
15162 .evaluate_vertex_immutable(vertex_id)
15163 .unwrap_or_else(LiteralValue::Error);
15164 let effects = self.plan_vertex_effects(vertex_id, value.clone())?;
15165 self.resource_checkpoint(0)?;
15167 let mut delta = delta;
15168 for effect in &effects {
15169 self.apply_effect_with_computed_writes(effect, delta.as_deref_mut(), None, None)?;
15170 }
15171 return Ok(value);
15172 }
15173
15174 let mut delta = delta;
15175
15176 let kind = self.graph.get_vertex_kind(vertex_id);
15178 let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
15179
15180 let view = match kind {
15181 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
15182 if let Some(view) = self.graph.formula_view(vertex_id) {
15183 view
15184 } else {
15185 return Ok(LiteralValue::Number(0.0));
15186 }
15187 }
15188 VertexKind::Empty | VertexKind::Cell => {
15189 if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
15190 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15191 let row = cell_ref.coord.row() + 1;
15192 let col = cell_ref.coord.col() + 1;
15193 if let Some(v) = self.read_cell_value(sheet_name, row, col) {
15194 return Ok(v);
15195 }
15196 }
15197 return Ok(LiteralValue::Number(0.0));
15198 }
15199 VertexKind::NamedScalar => {
15200 let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
15201 return Ok(value);
15202 }
15203 VertexKind::NamedArray => {
15204 let value = self.evaluate_named_array(vertex_id, sheet_id)?;
15205 return Ok(value);
15206 }
15207 VertexKind::InfiniteRange
15208 | VertexKind::Range
15209 | VertexKind::External
15210 | VertexKind::Table => {
15211 return Ok(LiteralValue::Number(0.0));
15213 }
15214 };
15215
15216 let sheet_name = self.graph.sheet_name(sheet_id);
15218 let cell_ref = self
15219 .graph
15220 .get_cell_ref(vertex_id)
15221 .expect("cell ref for vertex");
15222 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15223 .with_resolved_functions(&self.resolved_functions)
15224 .with_arena_cells(self);
15225
15226 let result = interpreter.evaluate_formula_view(
15227 view,
15228 self.graph.data_store(),
15229 self.graph.sheet_reg(),
15230 );
15231
15232 match result {
15234 Ok(cv) => {
15235 let derived_format = cv.format_id();
15236 self.record_derived_format(vertex_id, derived_format);
15237 let oversized_range = if matches!(cv, crate::traits::CalcValue::Range(_))
15238 && self.graph.fixed_single_arrays.contains(&vertex_id)
15239 {
15240 None
15241 } else {
15242 crate::engine::result_finalization::range_spill_error(
15243 &cv,
15244 self.config.spill.max_spill_cells,
15245 )
15246 };
15247 let is_oversized_range = oversized_range.is_some();
15248 let result_literal = if let Some(error) = oversized_range {
15249 drop(cv);
15250 LiteralValue::Error(error)
15251 } else {
15252 self.materialize_formula_result(vertex_id, cv)
15253 };
15254 let output_sheet_name = sheet_name.to_string();
15255 self.write_computed_overlay_format_0based(
15256 &output_sheet_name,
15257 cell_ref.coord.row(),
15258 cell_ref.coord.col(),
15259 derived_format,
15260 );
15261 if is_oversized_range {
15262 let LiteralValue::Error(error) = result_literal else {
15263 unreachable!()
15264 };
15265 self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
15266 return Ok(self.publish_oversized_spill(
15267 vertex_id,
15268 error,
15269 delta.as_deref_mut(),
15270 ));
15271 }
15272 match result_literal {
15273 LiteralValue::Array(rows) => {
15274 self.graph
15276 .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
15277 let anchor = self
15279 .graph
15280 .get_cell_ref(vertex_id)
15281 .expect("cell ref for vertex");
15282 let sheet_id = anchor.sheet_id;
15283 let h = rows.len() as u32;
15284 let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
15285
15286 let spill_cells = (h as u64).saturating_mul(w as u64);
15288 if spill_cells > self.config.spill.max_spill_cells as u64 {
15289 let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15290 .with_message("SpillTooLarge")
15291 .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15292 expected_rows: h,
15293 expected_cols: w,
15294 });
15295 return Ok(self.publish_oversized_spill(
15296 vertex_id,
15297 spill_err,
15298 delta.as_deref_mut(),
15299 ));
15300 }
15301 const PACKED_MAX_ROW: u32 = 1_048_575; const PACKED_MAX_COL: u32 = 16_383; let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
15305 let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
15306 if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
15307 self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
15308 let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15309 .with_message("Spill exceeds sheet bounds")
15310 .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15311 expected_rows: h,
15312 expected_cols: w,
15313 });
15314 let spill_val = LiteralValue::Error(spill_err.clone());
15315 if let Some(d) = delta.as_deref_mut() {
15316 let old = self
15317 .read_cell_value(
15318 self.graph.sheet_name(anchor.sheet_id),
15319 anchor.coord.row() + 1,
15320 anchor.coord.col() + 1,
15321 )
15322 .unwrap_or(LiteralValue::Empty);
15323 if old != spill_val {
15324 d.record_cell(
15325 anchor.sheet_id,
15326 anchor.coord.row(),
15327 anchor.coord.col(),
15328 );
15329 }
15330 }
15331 self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15332 if self.config.arrow_storage_enabled
15333 && self.config.delta_overlay_enabled
15334 && self.config.write_formula_overlay_enabled
15335 {
15336 let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15337 self.mirror_value_to_computed_overlay(
15338 &sheet_name,
15339 anchor.coord.row() + 1,
15340 anchor.coord.col() + 1,
15341 &spill_val,
15342 );
15343 }
15344 return Ok(spill_val);
15345 }
15346 let mut targets = Vec::new();
15347 for r in 0..h {
15348 for c in 0..w {
15349 targets.push(self.graph.make_cell_ref_internal(
15350 sheet_id,
15351 anchor.coord.row() + r,
15352 anchor.coord.col() + c,
15353 ));
15354 }
15355 }
15356
15357 match self.spill_mgr.reserve(
15359 vertex_id,
15360 anchor,
15361 SpillShape { rows: h, cols: w },
15362 SpillMeta {
15363 epoch: self.recalc_epoch,
15364 config: self.config.spill,
15365 },
15366 ) {
15367 Ok(()) => {
15368 if let Err(e) = self.commit_spill_and_mirror(
15372 vertex_id,
15373 &targets,
15374 rows.clone(),
15375 delta.as_deref_mut(),
15376 ) {
15377 if e.kind != ExcelErrorKind::Spill {
15378 return Err(e);
15379 }
15380 self.clear_spill_projection_and_mirror(
15382 vertex_id,
15383 delta.as_deref_mut(),
15384 );
15385 if let Some(d) = delta.as_deref_mut() {
15386 let old = self
15387 .read_cell_value(
15388 self.graph.sheet_name(anchor.sheet_id),
15389 anchor.coord.row() + 1,
15390 anchor.coord.col() + 1,
15391 )
15392 .unwrap_or(LiteralValue::Empty);
15393 let new = LiteralValue::Error(e.clone());
15394 if old != new {
15395 d.record_cell(
15396 anchor.sheet_id,
15397 anchor.coord.row(),
15398 anchor.coord.col(),
15399 );
15400 }
15401 }
15402 let err_val = LiteralValue::Error(e.clone());
15403 self.graph.update_vertex_value_ref(vertex_id, &err_val);
15404 if self.config.arrow_storage_enabled
15405 && self.config.delta_overlay_enabled
15406 && self.config.write_formula_overlay_enabled
15407 {
15408 let sheet_name =
15409 self.graph.sheet_name(anchor.sheet_id).to_string();
15410 self.mirror_value_to_computed_overlay(
15411 &sheet_name,
15412 anchor.coord.row() + 1,
15413 anchor.coord.col() + 1,
15414 &err_val,
15415 );
15416 }
15417 return Ok(err_val);
15418 }
15419 let top_left = rows
15421 .first()
15422 .and_then(|r| r.first())
15423 .cloned()
15424 .unwrap_or(LiteralValue::Empty);
15425 self.graph.update_vertex_value_ref(vertex_id, &top_left);
15426 Ok(top_left)
15427 }
15428 Err(e) => {
15429 self.clear_spill_projection_and_mirror(
15430 vertex_id,
15431 delta.as_deref_mut(),
15432 );
15433 let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15434 .with_message(e.message().unwrap_or("Spill blocked"))
15435 .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15436 expected_rows: h,
15437 expected_cols: w,
15438 });
15439 let spill_val = LiteralValue::Error(spill_err.clone());
15440 if let Some(d) = delta.as_deref_mut() {
15441 let old = self
15442 .read_cell_value(
15443 self.graph.sheet_name(anchor.sheet_id),
15444 anchor.coord.row() + 1,
15445 anchor.coord.col() + 1,
15446 )
15447 .unwrap_or(LiteralValue::Empty);
15448 if old != spill_val {
15449 d.record_cell(
15450 anchor.sheet_id,
15451 anchor.coord.row(),
15452 anchor.coord.col(),
15453 );
15454 }
15455 }
15456 self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15457 if self.config.arrow_storage_enabled
15458 && self.config.delta_overlay_enabled
15459 && self.config.write_formula_overlay_enabled
15460 {
15461 let sheet_name =
15462 self.graph.sheet_name(anchor.sheet_id).to_string();
15463 self.mirror_value_to_computed_overlay(
15464 &sheet_name,
15465 anchor.coord.row() + 1,
15466 anchor.coord.col() + 1,
15467 &spill_val,
15468 );
15469 }
15470 Ok(spill_val)
15471 }
15472 }
15473 }
15474 other => {
15475 let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
15477 if let Some(d) = delta.as_deref_mut()
15478 && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
15479 {
15480 if spill_cells.is_empty() {
15481 let old = self
15482 .read_cell_value(
15483 self.graph.sheet_name(anchor.sheet_id),
15484 anchor.coord.row() + 1,
15485 anchor.coord.col() + 1,
15486 )
15487 .unwrap_or(LiteralValue::Empty);
15488 if old != other {
15489 d.record_cell(
15490 anchor.sheet_id,
15491 anchor.coord.row(),
15492 anchor.coord.col(),
15493 );
15494 }
15495 } else {
15496 for cell in spill_cells.iter() {
15497 let sheet_name = self.graph.sheet_name(cell.sheet_id);
15498 let old = self
15499 .read_cell_value(
15500 sheet_name,
15501 cell.coord.row() + 1,
15502 cell.coord.col() + 1,
15503 )
15504 .unwrap_or(LiteralValue::Empty);
15505 let new = if cell.sheet_id == anchor.sheet_id
15506 && cell.coord.row() == anchor.coord.row()
15507 && cell.coord.col() == anchor.coord.col()
15508 {
15509 other.clone()
15510 } else {
15511 LiteralValue::Empty
15512 };
15513 Self::record_cell_if_changed(d, cell, &old, &new);
15514 }
15515 }
15516 }
15517 self.graph.clear_spill_region(vertex_id);
15518 self.freshness_note_spill_change(&spill_cells, &[]);
15519 if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
15520 self.record_structural_change(scope);
15521 }
15522 if self.config.arrow_storage_enabled
15523 && self.config.delta_overlay_enabled
15524 && self.config.write_formula_overlay_enabled
15525 {
15526 let empty = LiteralValue::Empty;
15527 for cell in spill_cells.iter() {
15528 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
15529 self.mirror_value_to_computed_overlay(
15530 &sheet_name,
15531 cell.coord.row() + 1,
15532 cell.coord.col() + 1,
15533 &empty,
15534 );
15535 }
15536 }
15537 self.graph.update_vertex_value_ref(vertex_id, &other);
15538 if self.config.arrow_storage_enabled
15540 && self.config.delta_overlay_enabled
15541 && self.config.write_formula_overlay_enabled
15542 {
15543 let anchor = self
15544 .graph
15545 .get_cell_ref(vertex_id)
15546 .expect("cell ref for vertex");
15547 let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15548 self.mirror_value_to_computed_overlay(
15549 &sheet_name,
15550 anchor.coord.row() + 1,
15551 anchor.coord.col() + 1,
15552 &other,
15553 );
15554 }
15555 Ok(other)
15556 }
15557 }
15558 }
15559 Err(e) => {
15560 let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
15563 let err_val = LiteralValue::Error(e.clone());
15564 if let Some(d) = delta
15565 && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
15566 {
15567 if spill_cells.is_empty() {
15568 let old = self
15569 .read_cell_value(
15570 self.graph.sheet_name(anchor.sheet_id),
15571 anchor.coord.row() + 1,
15572 anchor.coord.col() + 1,
15573 )
15574 .unwrap_or(LiteralValue::Empty);
15575 if old != err_val {
15576 d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
15577 }
15578 } else {
15579 for cell in spill_cells.iter() {
15580 let sheet_name = self.graph.sheet_name(cell.sheet_id);
15581 let old = self
15582 .read_cell_value(
15583 sheet_name,
15584 cell.coord.row() + 1,
15585 cell.coord.col() + 1,
15586 )
15587 .unwrap_or(LiteralValue::Empty);
15588 let new = if cell.sheet_id == anchor.sheet_id
15589 && cell.coord.row() == anchor.coord.row()
15590 && cell.coord.col() == anchor.coord.col()
15591 {
15592 err_val.clone()
15593 } else {
15594 LiteralValue::Empty
15595 };
15596 Self::record_cell_if_changed(d, cell, &old, &new);
15597 }
15598 }
15599 }
15600 self.graph.clear_spill_region(vertex_id);
15601 self.freshness_note_spill_change(&spill_cells, &[]);
15602 if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
15603 self.record_structural_change(scope);
15604 }
15605 if self.config.arrow_storage_enabled
15606 && self.config.delta_overlay_enabled
15607 && self.config.write_formula_overlay_enabled
15608 {
15609 let empty = LiteralValue::Empty;
15610 for cell in spill_cells.iter() {
15611 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
15612 self.mirror_value_to_computed_overlay(
15613 &sheet_name,
15614 cell.coord.row() + 1,
15615 cell.coord.col() + 1,
15616 &empty,
15617 );
15618 }
15619 }
15620 self.graph.update_vertex_value_ref(vertex_id, &err_val);
15621 if self.config.arrow_storage_enabled
15622 && self.config.delta_overlay_enabled
15623 && self.config.write_formula_overlay_enabled
15624 {
15625 let anchor = self
15626 .graph
15627 .get_cell_ref(vertex_id)
15628 .expect("cell ref for vertex");
15629 let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15630 self.mirror_value_to_computed_overlay(
15631 &sheet_name,
15632 anchor.coord.row() + 1,
15633 anchor.coord.col() + 1,
15634 &err_val,
15635 );
15636 }
15637 Ok(err_val)
15638 }
15639 }
15640 }
15641
15642 fn evaluate_named_scalar(
15643 &mut self,
15644 vertex_id: VertexId,
15645 sheet_id: SheetId,
15646 ) -> Result<LiteralValue, ExcelError> {
15647 let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
15648 ExcelError::new(ExcelErrorKind::Name)
15649 .with_message("Named range metadata missing".to_string())
15650 })?;
15651
15652 match &named_range.definition {
15653 NamedDefinition::Cell(cell_ref) => {
15654 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15655 let row = cell_ref.coord.row() + 1;
15656 let col = cell_ref.coord.col() + 1;
15657
15658 if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
15659 && matches!(
15660 self.graph.get_vertex_kind(dep_vertex),
15661 VertexKind::FormulaScalar | VertexKind::FormulaArray
15662 )
15663 {
15664 let value = self.evaluate_vertex(dep_vertex)?;
15666 self.graph.update_vertex_value_ref(vertex_id, &value);
15667 Ok(value)
15668 } else {
15669 let value = self
15670 .read_cell_value(sheet_name, row, col)
15671 .unwrap_or(LiteralValue::Empty);
15672 self.graph.update_vertex_value_ref(vertex_id, &value);
15673 Ok(value)
15674 }
15675 }
15676 NamedDefinition::Literal(v) => {
15677 let out = v.clone();
15678 self.graph.update_vertex_value_ref(vertex_id, &out);
15679 Ok(out)
15680 }
15681 NamedDefinition::Formula { ast, .. } => {
15682 let _origin = self
15683 .operation_diagnostic_collector
15684 .as_ref()
15685 .map(|c| c.named_scope(vertex_id));
15686 let context_sheet = match named_range.scope {
15687 NameScope::Sheet(id) => id,
15688 NameScope::Workbook => sheet_id,
15689 };
15690 let sheet_name = self.graph.sheet_name(context_sheet);
15691 let cell_ref = self
15692 .graph
15693 .get_cell_ref(vertex_id)
15694 .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
15695 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15696 .with_resolved_functions(&self.resolved_functions)
15697 .with_arena_cells(self);
15698 match interpreter.evaluate_ast(ast) {
15699 Ok(cv) => {
15700 let value = cv.into_literal();
15701 match value {
15702 LiteralValue::Array(_) => {
15703 let err = ExcelError::new(ExcelErrorKind::NImpl)
15704 .with_message("Array result in scalar named range".to_string());
15705 let err_val = LiteralValue::Error(err.clone());
15706 self.graph.update_vertex_value_ref(vertex_id, &err_val);
15707 Ok(err_val)
15708 }
15709 other => {
15710 self.graph.update_vertex_value_ref(vertex_id, &other);
15711 Ok(other)
15712 }
15713 }
15714 }
15715 Err(err) => {
15716 let err_val = LiteralValue::Error(err.clone());
15717 self.graph.update_vertex_value_ref(vertex_id, &err_val);
15718 Ok(err_val)
15719 }
15720 }
15721 }
15722 NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
15723 .with_message("Range-valued name evaluated as scalar".to_string())),
15724 }
15725 }
15726
15727 fn evaluate_named_array(
15728 &mut self,
15729 vertex_id: VertexId,
15730 sheet_id: SheetId,
15731 ) -> Result<LiteralValue, ExcelError> {
15732 let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
15733 ExcelError::new(ExcelErrorKind::Name)
15734 .with_message("Named range metadata missing".to_string())
15735 })?;
15736
15737 let out = match &named_range.definition {
15738 NamedDefinition::Range(range_ref) => {
15739 if range_ref.start.sheet_id != range_ref.end.sheet_id {
15740 return Err(ExcelError::new(ExcelErrorKind::Ref)
15741 .with_message("Named range cannot span sheets".to_string()));
15742 }
15743
15744 let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
15745 let sr0 = range_ref.start.coord.row();
15746 let sc0 = range_ref.start.coord.col();
15747 let er0 = range_ref.end.coord.row();
15748 let ec0 = range_ref.end.coord.col();
15749 if sr0 > er0 || sc0 > ec0 {
15750 return Err(ExcelError::new(ExcelErrorKind::Ref)
15751 .with_message("Invalid named range bounds".to_string()));
15752 }
15753
15754 let h = (er0 - sr0 + 1) as usize;
15755 let w = (ec0 - sc0 + 1) as usize;
15756 let cell_count = (h as u64).saturating_mul(w as u64);
15757 if cell_count > self.config.spill.max_spill_cells as u64 {
15758 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
15759 "Named range too large to materialize as an array".to_string(),
15760 ));
15761 }
15762
15763 let mut rows = Vec::with_capacity(h);
15764 for r0 in sr0..=er0 {
15765 let mut row = Vec::with_capacity(w);
15766 for c0 in sc0..=ec0 {
15767 let v = self
15768 .read_cell_value(sheet_name, r0 + 1, c0 + 1)
15769 .unwrap_or(LiteralValue::Empty);
15770 row.push(v);
15771 }
15772 rows.push(row);
15773 }
15774 LiteralValue::Array(rows)
15775 }
15776 NamedDefinition::Cell(cell_ref) => {
15777 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15778 let row = cell_ref.coord.row() + 1;
15779 let col = cell_ref.coord.col() + 1;
15780 let v = self
15781 .read_cell_value(sheet_name, row, col)
15782 .unwrap_or(LiteralValue::Empty);
15783 LiteralValue::Array(vec![vec![v]])
15784 }
15785 NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
15786 NamedDefinition::Formula { ast, .. } => {
15787 let _origin = self
15788 .operation_diagnostic_collector
15789 .as_ref()
15790 .map(|c| c.named_scope(vertex_id));
15791 let context_sheet = match named_range.scope {
15792 NameScope::Sheet(id) => id,
15793 NameScope::Workbook => sheet_id,
15794 };
15795 let sheet_name = self.graph.sheet_name(context_sheet);
15796 let cell_ref = self
15797 .graph
15798 .get_cell_ref(vertex_id)
15799 .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
15800 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15801 .with_resolved_functions(&self.resolved_functions)
15802 .with_arena_cells(self);
15803 match interpreter.evaluate_ast(ast) {
15804 Ok(cv) => {
15805 let v = cv.into_literal();
15806 match v {
15807 LiteralValue::Array(_) => v,
15808 other => LiteralValue::Array(vec![vec![other]]),
15809 }
15810 }
15811 Err(err) => LiteralValue::Error(err),
15812 }
15813 }
15814 };
15815
15816 self.graph.update_vertex_value_ref(vertex_id, &out);
15817 Ok(out)
15818 }
15819
15820 fn replan_exhausted_error(&self, limit: usize, context: &str) -> ExcelError {
15821 crate::engine::ResourceLedgerError::Exhausted(
15822 formualizer_common::ResourceExhaustionDetail {
15823 reason: formualizer_common::ResourceExhaustionReason::WorkUnits,
15824 limit: limit as u64,
15825 observed: limit.saturating_add(1) as u64,
15826 request_id: self
15827 .active_evaluation_resource_request
15828 .as_ref()
15829 .map(|stats| stats.request_id),
15830 },
15831 )
15832 .into_excel_error()
15833 .with_message(format!("{context} did not converge after {limit} replans"))
15834 }
15835
15836 fn transient_target_preparation_stale(error: &ExcelError) -> bool {
15837 matches!(
15838 error.extra(),
15839 formualizer_common::ExcelErrorExtra::PreparationStale {
15840 reason: formualizer_common::PreparationStaleReason::Semantic
15841 | formualizer_common::PreparationStaleReason::Provider
15842 }
15843 )
15844 }
15845
15846 fn prepare_graph_for_routed_evaluation(
15847 &mut self,
15848 targets: &[crate::engine::EvaluationTarget],
15849 options: &crate::engine::TargetEvalOptions<'_>,
15850 ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
15851 const MAX_TRANSIENT_PREPARATION_RETRIES: usize = 2;
15852 let mut retries = 0usize;
15853 loop {
15854 match self.prepare_graph_for_targets_unobserved(targets, options) {
15855 Err(error)
15856 if Self::transient_target_preparation_stale(&error)
15857 && retries < MAX_TRANSIENT_PREPARATION_RETRIES =>
15858 {
15859 retries = retries.saturating_add(1);
15860 }
15861 result => return result,
15862 }
15863 }
15864 }
15865
15866 fn evaluate_mixed_targets(
15867 &mut self,
15868 targets: &[crate::engine::EvaluationTarget],
15869 delta: Option<&mut DeltaCollector>,
15870 ) -> Result<EvalResult, ExcelError> {
15871 let _source_cache = self.source_cache_session();
15872 let cancel = self.active_cancel_flag.clone();
15873 let options = crate::engine::TargetEvalOptions {
15874 request_id: self
15875 .active_evaluation_resource_request
15876 .as_ref()
15877 .map(|stats| stats.request_id),
15878 cancel,
15879 deadline: None,
15880 budgets: None,
15881 opaque_policy: crate::engine::OpaquePreparePolicy::Widen,
15882 };
15883 self.prepare_and_execute_target_recipe(targets, &options, delta)
15884 }
15885
15886 fn prepare_and_execute_target_recipe(
15887 &mut self,
15888 targets: &[crate::engine::EvaluationTarget],
15889 options: &crate::engine::TargetEvalOptions<'_>,
15890 delta: Option<&mut DeltaCollector>,
15891 ) -> Result<EvalResult, ExcelError> {
15892 let preparation = self.prepare_graph_for_routed_evaluation(targets, options)?;
15893 self.execute_prepared_target_recipe(targets, &preparation.widened_scope, delta)
15894 }
15895
15896 fn execute_prepared_target_recipe(
15897 &mut self,
15898 targets: &[crate::engine::EvaluationTarget],
15899 scope: &crate::engine::PrepareScope,
15900 delta: Option<&mut DeltaCollector>,
15901 ) -> Result<EvalResult, ExcelError> {
15902 self.require_unified_authority()?;
15903 if matches!(scope, crate::engine::PrepareScope::Workbook)
15904 && let Some(stats) = self.active_evaluation_resource_request.as_mut()
15905 {
15906 stats.workbook_exact_attempts = stats.workbook_exact_attempts.max(1);
15907 }
15908 let mut roots = self.resolve_target_producers(targets)?;
15909 if let crate::engine::PrepareScope::Sheets(sheets) = scope {
15910 let request_id = self
15911 .active_evaluation_resource_request
15912 .as_ref()
15913 .map(|request| request.request_id);
15914 let root_count = roots.len();
15915 let mut widened_roots =
15916 OrderedTargetProducers::from_ordered(std::mem::take(&mut roots))
15917 .map_err(|_| target_root_allocation_error(root_count, request_id))?;
15918 let sheet_ids = sheets
15919 .iter()
15920 .filter_map(|sheet| self.graph.sheet_id(sheet))
15921 .collect::<FxHashSet<_>>();
15922 for vertex in self.graph.formula_vertices() {
15923 if sheet_ids.contains(&self.graph.get_vertex_sheet_id(vertex)) {
15924 widened_roots
15925 .push(crate::engine::target_preparation::TargetProducer::Legacy(
15926 vertex,
15927 ))
15928 .map_err(|_| {
15929 target_root_allocation_error(widened_roots.len() + 1, request_id)
15930 })?;
15931 }
15932 }
15933 roots = widened_roots.into_vec();
15934 }
15935 self.begin_evaluation_request();
15936 #[cfg(any(test, feature = "legacy_oracle"))]
15937 self.graph.flush_pending_edge_deltas();
15938 let workbook_scope = matches!(scope, crate::engine::PrepareScope::Workbook);
15939 if workbook_scope {
15940 if let Some(delta) = delta {
15941 self.evaluate_all_with_delta_collector(delta)
15942 } else {
15943 self.evaluate_all_legacy_impl()
15944 }
15945 } else {
15946 self.evaluate_legacy_target_roots(&roots, delta)
15947 }
15948 }
15949
15950 fn legacy_coordinate_targets(
15951 &mut self,
15952 targets: &[(&str, u32, u32)],
15953 ) -> Vec<crate::engine::EvaluationTarget> {
15954 targets
15955 .iter()
15956 .map(|(sheet, row, col)| {
15957 self.graph.sheet_id_mut(sheet);
15960 crate::engine::EvaluationTarget::Cell {
15961 sheet: (*sheet).to_string(),
15962 row: *row,
15963 col: *col,
15964 }
15965 })
15966 .collect()
15967 }
15968
15969 pub fn evaluate_targets(
15971 &mut self,
15972 targets: &[crate::engine::EvaluationTarget],
15973 ) -> Result<EvalResult, ExcelError> {
15974 self.ensure_not_poisoned()?;
15975 self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
15976 engine.observe_function_semantic_epoch()?;
15977 engine.validate_deterministic_mode()?;
15978 engine.evaluate_mixed_targets(targets, None)
15979 })
15980 }
15981
15982 pub fn evaluate_targets_with_options(
15984 &mut self,
15985 targets: &[crate::engine::EvaluationTarget],
15986 options: crate::engine::TargetEvalOptions<'_>,
15987 ) -> Result<EvalResult, ExcelError> {
15988 self.ensure_not_poisoned()?;
15989 self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
15990 engine.active_cancel_flag = options.cancel.clone();
15991 engine.active_evaluation_deadline = options.deadline;
15992 let result = (|| {
15993 engine.cancellation_checkpoint("Evaluation cancelled before target preparation")?;
15994 engine.observe_function_semantic_epoch()?;
15995 engine.validate_deterministic_mode()?;
15996 let _source_cache = engine.source_cache_session();
15997 engine.prepare_and_execute_target_recipe(targets, &options, None)
15998 })();
15999 engine.active_cancel_flag = None;
16000 engine.active_evaluation_deadline = None;
16001 result
16002 })
16003 }
16004
16005 pub fn evaluate_targets_with_delta(
16007 &mut self,
16008 targets: &[crate::engine::EvaluationTarget],
16009 ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
16010 self.ensure_not_poisoned()?;
16011 self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
16012 engine.observe_function_semantic_epoch()?;
16013 engine.validate_deterministic_mode()?;
16014 let mut collector = DeltaCollector::new(DeltaMode::Cells);
16015 let result = engine.evaluate_mixed_targets(targets, Some(&mut collector))?;
16016 Ok((result, collector.finish_target()))
16017 })
16018 }
16019
16020 pub fn evaluate_until(
16022 &mut self,
16023 targets: &[(&str, u32, u32)],
16024 ) -> Result<EvalResult, ExcelError> {
16025 self.ensure_not_poisoned()?;
16026 self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
16027 engine.evaluate_until_unobserved(targets)
16028 })
16029 }
16030
16031 fn evaluate_until_unobserved(
16032 &mut self,
16033 targets: &[(&str, u32, u32)],
16034 ) -> Result<EvalResult, ExcelError> {
16035 self.observe_function_semantic_epoch()?;
16036 let targets = self.legacy_coordinate_targets(targets);
16037 self.evaluate_mixed_targets(&targets, None)
16038 }
16039
16040 fn evaluate_until_with_delta_collector(
16041 &mut self,
16042 targets: &[(&str, u32, u32)],
16043 delta: &mut DeltaCollector,
16044 ) -> Result<EvalResult, ExcelError> {
16045 let targets = self.legacy_coordinate_targets(targets);
16046 self.evaluate_mixed_targets(&targets, Some(delta))
16047 }
16048
16049 fn evaluate_legacy_target_roots(
16050 &mut self,
16051 roots: &[crate::engine::target_preparation::TargetProducer],
16052 mut delta: Option<&mut DeltaCollector>,
16053 ) -> Result<EvalResult, ExcelError> {
16054 use crate::engine::target_preparation::TargetProducer;
16055 #[cfg(any(test, feature = "benchmark_internal"))]
16056 {
16057 self.recalc_reuse_probe
16058 .get_mut()
16059 .unwrap()
16060 .legacy_target_requests += 1;
16061 }
16062 let start = crate::instant::FzInstant::now();
16063 let root_vertices = roots
16064 .iter()
16065 .filter_map(|root| match root {
16066 TargetProducer::Legacy(vertex) | TargetProducer::Symbol(vertex) => Some(*vertex),
16067 TargetProducer::ValueOnly(_) => None,
16068 })
16069 .collect::<Vec<_>>();
16070 let mut computed_vertices = 0usize;
16071 let mut cycle_errors = 0usize;
16072 let mut replans = 0usize;
16073 const MAX_REPLAN: usize = 5;
16074 self.graph.authority_sync();
16075 loop {
16076 let (precedents_to_eval, old_vdeps) = self.demand_subgraph(&root_vertices)?;
16077 if precedents_to_eval.is_empty() {
16078 break;
16079 }
16080 #[cfg(any(test, feature = "benchmark_internal"))]
16081 {
16082 self.recalc_reuse_probe
16083 .get_mut()
16084 .unwrap()
16085 .target_schedule_builds += 1;
16086 }
16087 let schedule = {
16088 self.graph.authority_sync();
16089 let mut ledger = self.active_resource_ledger.take();
16090 let result = self.create_authority_schedule(
16091 &precedents_to_eval,
16092 &old_vdeps,
16093 ledger.as_mut(),
16094 );
16095 self.active_resource_ledger = ledger;
16096 result?
16097 };
16098 self.begin_pass(&schedule);
16099 for (unit_index, &unit) in schedule.units.iter().enumerate() {
16100 self.cancellation_checkpoint("Evaluation cancelled before target schedule unit")?;
16101 match unit {
16102 ScheduleUnit::Cycle(index) => {
16103 if self.handle_cycle_unit(
16104 schedule.unit_cycle(index),
16105 delta.as_deref_mut(),
16106 None,
16107 None,
16108 )? > 0
16109 {
16110 cycle_errors = cycle_errors.saturating_add(1);
16111 }
16112 }
16113 ScheduleUnit::Layer(index) => {
16114 let layer = schedule.unit_layer(index);
16115 let evaluated = if let Some(delta) = delta.as_deref_mut() {
16116 if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16117 self.evaluate_layer_parallel_with_delta(layer, delta)?
16118 } else {
16119 self.evaluate_layer_sequential_with_delta(layer, delta)?
16120 }
16121 } else if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16122 self.evaluate_layer_parallel(layer)?
16123 } else {
16124 self.evaluate_layer_sequential(layer)?
16125 };
16126 computed_vertices = computed_vertices.saturating_add(evaluated);
16127 }
16128 }
16129 if self.stop_after_unit(&schedule, unit_index) {
16130 break;
16131 }
16132 }
16133 let changed = self.changed_virtual_dep_vertices(&precedents_to_eval, &old_vdeps);
16134 self.resource_checkpoint(0)?;
16135 if !self.finish_target_pass_dirty(&precedents_to_eval, &changed) {
16136 break;
16137 }
16138 if replans >= MAX_REPLAN {
16139 return Err(self.replan_exhausted_error(
16140 MAX_REPLAN,
16141 "targeted legacy dynamic dependency evaluation",
16142 ));
16143 }
16144 replans = replans.saturating_add(1);
16145 }
16146 self.redirty_for_next_recalc();
16147 Ok(EvalResult {
16148 computed_vertices,
16149 cycle_errors,
16150 elapsed: start.elapsed(),
16151 })
16152 }
16153
16154 pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
16156 self.ensure_not_poisoned()?;
16157 if self.has_staged_formulas() || self.staged_formula_index.has_packages() {
16158 return Err(
16159 Self::plan_stale(formualizer_common::PlanStaleReason::Staged).with_message(
16160 "compatibility recalculation plans require all staged formulas to be prepared",
16161 ),
16162 );
16163 }
16164 let key = self.recalc_plan_key();
16165 let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
16166 vertices.sort_unstable();
16167 let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
16168 let schedule = if vertices.is_empty() {
16169 crate::engine::Schedule {
16170 units: Vec::new(),
16171 layers: Vec::new(),
16172 cycles: Vec::new(),
16173 }
16174 } else {
16175 self.create_evaluation_schedule_uncached(&vertices, None)?.0
16176 };
16177 self.validate_recalc_plan_key(&key)?;
16178 Ok(RecalcPlan {
16179 key,
16180 kind: RecalcPlanKind::CompatibilityFull {
16181 schedule,
16182 has_dynamic_refs,
16183 },
16184 })
16185 }
16186
16187 pub fn build_recalc_plan_for_targets(
16189 &mut self,
16190 targets: &[crate::engine::EvaluationTarget],
16191 ) -> Result<RecalcPlan, ExcelError> {
16192 self.ensure_not_poisoned()?;
16193 self.build_recalc_plan_for_targets_with_options(
16194 targets,
16195 crate::engine::TargetEvalOptions::default(),
16196 )
16197 }
16198
16199 pub fn build_recalc_plan_for_targets_with_options(
16200 &mut self,
16201 targets: &[crate::engine::EvaluationTarget],
16202 options: crate::engine::TargetEvalOptions<'_>,
16203 ) -> Result<RecalcPlan, ExcelError> {
16204 self.ensure_not_poisoned()?;
16205 self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16206 engine.observe_function_semantic_epoch()?;
16207 engine.validate_deterministic_mode()?;
16208 let _source_cache = engine.source_cache_session();
16209 let preparation = engine.prepare_graph_for_routed_evaluation(targets, &options)?;
16210 #[cfg(any(test, feature = "legacy_oracle"))]
16211 engine.graph.flush_pending_edge_deltas();
16212 let topology = if matches!(
16213 preparation.widened_scope,
16214 crate::engine::PrepareScope::Workbook
16215 ) {
16216 RecalcTopology::Workbook
16217 } else {
16218 RecalcTopology::RunLocalRecipe
16219 };
16220 Ok(RecalcPlan {
16221 key: engine.recalc_plan_key(),
16222 kind: RecalcPlanKind::Target {
16223 targets: targets.to_vec(),
16224 scope: preparation.widened_scope,
16225 topology,
16226 dynamic_policy: DynamicPlanPolicy::BoundedTargetReplan,
16227 },
16228 })
16229 })
16230 }
16231
16232 pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
16234 self.ensure_not_poisoned()?;
16235 self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16236 engine.evaluate_recalc_plan_unobserved(plan)
16237 })
16238 }
16239
16240 pub fn evaluate_recalc_plan_with_controls(
16241 &mut self,
16242 plan: &RecalcPlan,
16243 cancel: Option<crate::engine::CancelToken>,
16244 deadline: Option<Instant>,
16245 ) -> Result<EvalResult, ExcelError> {
16246 self.ensure_not_poisoned()?;
16247 self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16248 engine.active_cancel_flag = cancel.clone();
16249 engine.active_evaluation_deadline = deadline;
16250 let result = engine.evaluate_recalc_plan_unobserved(plan);
16251 engine.active_cancel_flag = None;
16252 engine.active_evaluation_deadline = None;
16253 result
16254 })
16255 }
16256
16257 fn evaluate_recalc_plan_unobserved(
16258 &mut self,
16259 plan: &RecalcPlan,
16260 ) -> Result<EvalResult, ExcelError> {
16261 #[cfg(any(test, feature = "legacy_oracle"))]
16262 self.graph.flush_pending_edge_deltas();
16263 self.validate_recalc_plan_key(&plan.key)?;
16264 self.cancellation_checkpoint("Evaluation cancelled before recalculation plan execution")?;
16265 self.validate_deterministic_mode()?;
16266
16267 match &plan.kind {
16268 RecalcPlanKind::Target {
16269 targets,
16270 scope,
16271 topology,
16272 dynamic_policy,
16273 } => {
16274 debug_assert_eq!(*dynamic_policy, DynamicPlanPolicy::BoundedTargetReplan);
16275 debug_assert_eq!(
16276 matches!(topology, RecalcTopology::Workbook),
16277 matches!(scope, crate::engine::PrepareScope::Workbook)
16278 );
16279 let _source_cache = self.source_cache_session();
16280 self.execute_prepared_target_recipe(targets, scope, None)
16281 }
16282 RecalcPlanKind::CompatibilityFull {
16283 schedule,
16284 has_dynamic_refs,
16285 } => {
16286 let _source_cache = self.source_cache_session();
16287 self.begin_evaluation_request();
16288 if *has_dynamic_refs {
16289 self.virtual_dep_fallback_activations =
16290 self.virtual_dep_fallback_activations.saturating_add(1);
16291 return self.evaluate_all_coordinator();
16292 }
16293
16294 let start = crate::instant::FzInstant::now();
16295 let dirty_vertices = self.graph.get_evaluation_vertices();
16296 if dirty_vertices.is_empty() {
16297 return Ok(EvalResult {
16298 computed_vertices: 0,
16299 cycle_errors: 0,
16300 elapsed: start.elapsed(),
16301 });
16302 }
16303
16304 let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
16305 let mut computed_vertices = 0;
16306 let mut cycle_errors = 0;
16307 for &unit in &schedule.units {
16308 self.cancellation_checkpoint(
16309 "Evaluation cancelled before recalculation plan schedule unit",
16310 )?;
16311 match unit {
16312 ScheduleUnit::Cycle(i) => {
16313 let stamped = self.handle_cycle_unit(
16314 schedule.unit_cycle(i),
16315 None,
16316 Some(&dirty_set),
16317 None,
16318 )?;
16319 if stamped > 0 {
16320 cycle_errors += 1;
16321 }
16322 }
16323 ScheduleUnit::Layer(i) => {
16324 let work: Vec<VertexId> = schedule
16325 .unit_layer(i)
16326 .vertices
16327 .iter()
16328 .copied()
16329 .filter(|v| dirty_set.contains(v))
16330 .collect();
16331 if work.is_empty() {
16332 continue;
16333 }
16334 let temp_layer = crate::engine::scheduler::Layer::new(work);
16335 if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
16336 computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
16337 } else {
16338 computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
16339 }
16340 }
16341 }
16342 }
16343
16344 self.resource_checkpoint(0)?;
16345 self.graph.clear_dirty_flags(&dirty_vertices);
16346 self.redirty_for_next_recalc();
16347 Ok(EvalResult {
16348 computed_vertices,
16349 cycle_errors,
16350 elapsed: start.elapsed(),
16351 })
16352 }
16353 }
16354 }
16355}
16356
16357impl<R> Engine<R>
16358where
16359 R: EvaluationContext,
16360{
16361 fn require_unified_authority(&mut self) -> Result<(), ExcelError> {
16366 self.graph
16367 .authority()
16368 .map(|_| ())
16369 .map_err(Self::authority_excel_error)
16370 }
16371
16372 fn authority_excel_error(error: crate::engine::authority::store::AuthorityError) -> ExcelError {
16373 use crate::engine::authority::store::AuthorityError;
16374 #[cfg(test)]
16378 if std::env::var_os("FZ_AUTHORITY_DEFERRED_TRACE").is_some() {
16379 eprintln!("M1B_AUTHORITY_ERROR {error:?}");
16380 }
16381 let kind = match error {
16382 AuthorityError::Unsupported { .. } => ExcelErrorKind::NImpl,
16383 _ => ExcelErrorKind::Error,
16384 };
16385 ExcelError::new(kind).with_message(format!("unified_authority: {error:?}"))
16386 }
16387
16388 pub fn pending_snapshot_iterative_members(&self) -> usize {
16391 self.snapshot_iterative_redirty_pending.len()
16392 }
16393
16394 pub fn evaluate_all_for_snapshot(
16408 &mut self,
16409 cancel: Option<crate::engine::CancelToken>,
16410 ) -> Result<EvalResult, ExcelError> {
16411 self.ensure_not_poisoned()?;
16412 if self.has_active_recalc_job() {
16415 return Err(Self::active_recalc_job_error());
16416 }
16417 struct SnapshotScope<'e, R: EvaluationContext>(&'e mut Engine<R>);
16421 impl<R: EvaluationContext> Drop for SnapshotScope<'_, R> {
16422 fn drop(&mut self) {
16423 self.0.snapshot_evaluation_active = false;
16424 if std::thread::panicking() {
16425 self.0.snapshot_volatile_redirty_pending = true;
16426 }
16427 }
16428 }
16429 self.snapshot_evaluation_active = true;
16430 let result = {
16431 let scope = SnapshotScope(self);
16432 match cancel {
16433 Some(cancel) => scope.0.evaluate_all_cancellable(cancel),
16434 None => scope.0.evaluate_all(),
16435 }
16436 };
16437 if result.is_err() {
16438 self.snapshot_volatile_redirty_pending = false;
16439 self.graph.redirty_volatiles();
16440 }
16441 result
16442 }
16443
16444 pub fn evaluate_all_until(
16463 &mut self,
16464 stop: super::EvaluationStop,
16465 ) -> Result<super::EvaluationProgress, ExcelError> {
16466 self.ensure_not_poisoned()?;
16467 if self.has_active_recalc_job() {
16468 return Err(Self::active_recalc_job_error());
16469 }
16470 let report = self.drive_recalc_step(stop)?;
16471 Ok(match report.result {
16472 Some(result) => super::EvaluationProgress::Complete(result),
16473 None => super::EvaluationProgress::Incomplete(super::IncompleteEvaluation {
16474 reason: if report.phase == super::StepPhase::Cancelled {
16475 super::StopReason::Cancelled
16476 } else {
16477 super::StopReason::Deadline
16478 },
16479 computed_vertices: report.step_computed_vertices,
16480 }),
16481 })
16482 }
16483
16484 pub fn has_pending_resumable_evaluation(&self) -> bool {
16486 self.resumable.is_some()
16487 }
16488
16489 fn capture_original_cycle_seeds(
16493 &self,
16494 cycle: &[VertexId],
16495 ) -> FxHashMap<VertexId, LiteralValue> {
16496 cycle
16497 .iter()
16498 .map(|&vertex| {
16499 let value = if let Some(cell) = self.graph.get_cell_ref(vertex) {
16500 let current = self
16501 .read_cell_value(
16502 self.graph.sheet_name(cell.sheet_id),
16503 cell.coord.row() + 1,
16504 cell.coord.col() + 1,
16505 )
16506 .unwrap_or(LiteralValue::Empty);
16507 if matches!(current, LiteralValue::Empty) {
16508 self.iterative_state_values
16509 .get(&vertex)
16510 .cloned()
16511 .unwrap_or(current)
16512 } else {
16513 current
16514 }
16515 } else {
16516 self.graph.get_value(vertex).unwrap_or(LiteralValue::Empty)
16517 };
16518 (vertex, value)
16519 })
16520 .collect()
16521 }
16522
16523 fn record_original_cycle_seeds(&mut self, schedule: &super::scheduler::Schedule) {
16524 if self.resumable.as_ref().unwrap().original_cycles_recorded {
16525 return;
16526 }
16527 for cycle in &schedule.cycles {
16528 let mut key = cycle.clone();
16529 key.sort_unstable();
16530 let seeds = self.capture_original_cycle_seeds(&key);
16531 let state = self.resumable.as_mut().unwrap();
16532 state.original_cycles.insert(key.clone());
16533 state.cycle_seeds.entry(key).or_insert(seeds);
16535 }
16536 let state = self.resumable.as_mut().unwrap();
16537 state.original_cycles_recorded = true;
16538 state.refresh_seed_retained_bytes();
16539 }
16540
16541 fn resumable_should_stop(
16542 &self,
16543 units: usize,
16544 stop_at: Option<crate::instant::FzInstant>,
16545 ) -> bool {
16546 #[cfg(test)]
16547 if self
16548 .resumable_stop_after_units_for_test
16549 .is_some_and(|n| units >= n.max(1))
16550 {
16551 return true;
16552 }
16553 units > 0 && stop_at.is_some_and(|t| crate::instant::FzInstant::now() >= t)
16554 }
16555
16556 pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
16558 self.ensure_not_poisoned()?;
16559 self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
16561 engine.evaluate_all_unobserved()
16562 })
16563 }
16564
16565 fn evaluate_all_unobserved(&mut self) -> Result<EvalResult, ExcelError> {
16566 debug_assert!(
16567 !self.graph.deferred_dirty_active(),
16568 "deferred-dirty scope leaked into evaluate_all: a begin_deferred_dirty \
16569 was not balanced by end_deferred_dirty"
16570 );
16571 self.observe_function_semantic_epoch()?;
16572 self.lookup_index_cache.reset_counters();
16573 let _source_cache = self.source_cache_session();
16574 self.validate_deterministic_mode()?;
16575 if self.config.defer_graph_building {
16576 self.build_graph_all()?;
16578 }
16579 self.evaluate_all_coordinator()
16580 }
16581
16582 fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
16585 self.require_unified_authority()?;
16586 self.begin_evaluation_request();
16587 self.evaluate_all_legacy_impl()
16588 }
16589
16590 fn legacy_pass_run_units(
16596 &mut self,
16597 schedule: &crate::engine::scheduler::Schedule,
16598 ) -> Result<(usize, usize), ExcelError> {
16599 let mut computed_vertices = 0;
16600 let mut cycle_count = 0;
16601 self.begin_pass(schedule);
16602 for (unit_index, &unit) in schedule.units.iter().enumerate() {
16603 match unit {
16604 ScheduleUnit::Cycle(i) => {
16605 if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0 {
16606 cycle_count += 1;
16607 }
16608 }
16609 ScheduleUnit::Layer(i) => {
16610 let layer = schedule.unit_layer(i);
16611 if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16612 computed_vertices += self.evaluate_layer_parallel(layer)?;
16613 } else {
16614 computed_vertices += self.evaluate_layer_sequential(layer)?;
16615 }
16616 }
16617 }
16618 if self.stop_after_unit(schedule, unit_index) {
16619 break;
16620 }
16621 }
16622 Ok((computed_vertices, cycle_count))
16623 }
16624
16625 fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
16632 self.reset_virtual_dep_telemetry_if_disabled();
16633 let _span_eval =
16634 crate::engine::trace::fz_span!(tracing::Level::INFO, "evaluate", "evaluate.legacy");
16635 let start = crate::instant::FzInstant::now();
16636 let mut computed_vertices = 0;
16637 let mut cycle_errors = 0;
16638 let mut replan_iterations = 0;
16639 const MAX_REPLAN: usize = 5;
16640 let mut telemetry = self
16641 .config
16642 .enable_virtual_dep_telemetry
16643 .then(|| self.start_virtual_dep_telemetry());
16644
16645 loop {
16646 let to_evaluate = self.graph.get_evaluation_vertices();
16647 if to_evaluate.is_empty() {
16648 if let Some(t) = telemetry.as_mut()
16649 && t.bailout_reason.is_none()
16650 {
16651 t.bailout_reason = Some("no_work");
16652 }
16653 break;
16654 }
16655
16656 let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16657 if let Some(t) = telemetry.as_mut() {
16658 Self::accumulate_schedule_meta(t, &meta);
16659 }
16660
16661 let (pass_computed, pass_cycles) = self.legacy_pass_run_units(&schedule)?;
16662 computed_vertices += pass_computed;
16663 cycle_errors += pass_cycles;
16664
16665 let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16667 if let Some(t) = telemetry.as_mut() {
16668 t.changed_vdeps_total += changed_vertices.len();
16669 }
16670
16671 self.resource_checkpoint(0)?;
16672 if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16673 if let Some(t) = telemetry.as_mut() {
16674 t.bailout_reason = Some("converged");
16675 }
16676 break;
16677 }
16678 if replan_iterations >= MAX_REPLAN {
16679 if let Some(mut t) = telemetry.take() {
16680 t.bailout_reason = Some("max_replan");
16681 t.replan_iterations = replan_iterations;
16682 self.last_virtual_dep_telemetry = t;
16683 }
16684 return Err(
16685 self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16686 );
16687 }
16688
16689 replan_iterations += 1;
16690 }
16691
16692 if let Some(mut t) = telemetry {
16693 t.replan_iterations = replan_iterations;
16694 self.last_virtual_dep_telemetry = t;
16695 }
16696
16697 self.redirty_for_next_recalc();
16699
16700 self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16702
16703 Ok(EvalResult {
16704 computed_vertices,
16705 cycle_errors,
16706 elapsed: start.elapsed(),
16707 })
16708 }
16709
16710 pub fn evaluate_all_with_target_delta(
16711 &mut self,
16712 ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
16713 self.ensure_not_poisoned()?;
16714 self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
16715 engine.observe_function_semantic_epoch()?;
16716 let mut collector = DeltaCollector::new(DeltaMode::Cells);
16717 let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
16718 Ok((result, collector.finish_target()))
16719 })
16720 }
16721
16722 pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
16723 self.ensure_not_poisoned()?;
16724 self.evaluate_all_with_delta_policy(EvalDeltaCompatibilityPolicy::Unlimited)
16725 }
16726
16727 pub fn evaluate_all_with_delta_policy(
16728 &mut self,
16729 policy: EvalDeltaCompatibilityPolicy,
16730 ) -> Result<(EvalResult, EvalDelta), ExcelError> {
16731 self.ensure_not_poisoned()?;
16732 self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
16733 engine.observe_function_semantic_epoch()?;
16734 let mut collector = DeltaCollector::new(DeltaMode::Cells);
16735 let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
16736 Ok((result, collector.finish_with_policy(policy)?))
16737 })
16738 }
16739
16740 fn evaluate_all_with_delta_collector(
16741 &mut self,
16742 delta: &mut DeltaCollector,
16743 ) -> Result<EvalResult, ExcelError> {
16744 let _source_cache = self.source_cache_session();
16745 if self.config.defer_graph_building {
16746 self.build_graph_all()?;
16747 }
16748 self.require_unified_authority()?;
16749 self.begin_evaluation_request();
16750 self.reset_virtual_dep_telemetry_if_disabled();
16751 let _span_eval = crate::engine::trace::fz_span!(
16752 tracing::Level::INFO,
16753 "evaluate",
16754 "evaluate.legacy_delta"
16755 );
16756 let start = crate::instant::FzInstant::now();
16757 let mut computed_vertices = 0;
16758 let mut cycle_errors = 0;
16759
16760 let mut replan_iterations = 0;
16761 const MAX_REPLAN: usize = 5;
16762 let mut telemetry = self
16763 .config
16764 .enable_virtual_dep_telemetry
16765 .then(|| self.start_virtual_dep_telemetry());
16766
16767 loop {
16768 let to_evaluate = self.graph.get_evaluation_vertices();
16769 if to_evaluate.is_empty() {
16770 if let Some(t) = telemetry.as_mut()
16771 && t.bailout_reason.is_none()
16772 {
16773 t.bailout_reason = Some("no_work");
16774 }
16775 break;
16776 }
16777
16778 let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16779 if let Some(t) = telemetry.as_mut() {
16780 Self::accumulate_schedule_meta(t, &meta);
16781 }
16782
16783 self.begin_pass(&schedule);
16784 for (unit_index, &unit) in schedule.units.iter().enumerate() {
16785 match unit {
16786 ScheduleUnit::Cycle(i) => {
16787 if self.handle_cycle_unit(
16788 schedule.unit_cycle(i),
16789 Some(delta),
16790 None,
16791 None,
16792 )? > 0
16793 {
16794 cycle_errors += 1;
16795 }
16796 }
16797 ScheduleUnit::Layer(i) => {
16798 let layer = schedule.unit_layer(i);
16799 if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16800 computed_vertices +=
16801 self.evaluate_layer_parallel_with_delta(layer, delta)?;
16802 } else {
16803 computed_vertices +=
16804 self.evaluate_layer_sequential_with_delta(layer, delta)?;
16805 }
16806 }
16807 }
16808 if self.stop_after_unit(&schedule, unit_index) {
16809 break;
16810 }
16811 }
16812
16813 let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16814 if let Some(t) = telemetry.as_mut() {
16815 t.changed_vdeps_total += changed_vertices.len();
16816 }
16817 self.resource_checkpoint(0)?;
16818 if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16819 if let Some(t) = telemetry.as_mut() {
16820 t.bailout_reason = Some("converged");
16821 }
16822 break;
16823 }
16824 if replan_iterations >= MAX_REPLAN {
16825 if let Some(mut t) = telemetry.take() {
16826 t.bailout_reason = Some("max_replan");
16827 t.replan_iterations = replan_iterations;
16828 self.last_virtual_dep_telemetry = t;
16829 }
16830 return Err(
16831 self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16832 );
16833 }
16834 replan_iterations += 1;
16835 }
16836
16837 if let Some(mut t) = telemetry {
16838 t.replan_iterations = replan_iterations;
16839 self.last_virtual_dep_telemetry = t;
16840 }
16841
16842 self.redirty_for_next_recalc();
16843 self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16844
16845 Ok(EvalResult {
16846 computed_vertices,
16847 cycle_errors,
16848 elapsed: start.elapsed(),
16849 })
16850 }
16851
16852 pub fn evaluate_cell(
16862 &mut self,
16863 sheet: &str,
16864 row: u32,
16865 col: u32,
16866 ) -> Result<Option<LiteralValue>, ExcelError> {
16867 self.ensure_not_poisoned()?;
16868 self.observe_evaluation_resource_request(EvaluationRequestKind::Cell, |engine| {
16869 engine.evaluate_cell_unobserved(sheet, row, col)
16870 })
16871 }
16872
16873 fn evaluate_cell_unobserved(
16874 &mut self,
16875 sheet: &str,
16876 row: u32,
16877 col: u32,
16878 ) -> Result<Option<LiteralValue>, ExcelError> {
16879 if row == 0 || col == 0 {
16880 return Err(ExcelError::new(ExcelErrorKind::Ref)
16881 .with_message("Row and column must be >= 1".to_string()));
16882 }
16883
16884 let result = self.evaluate_cells(&[(sheet, row, col)])?;
16885
16886 match result.len() {
16887 0 => Ok(None),
16888 1 => {
16889 let v = result.into_iter().next().unwrap();
16890 Ok(v)
16891 }
16892 _ => unreachable!("evaluate_cells returned unexpected length"),
16893 }
16894 }
16895
16896 pub fn evaluate_cells(
16903 &mut self,
16904 targets: &[(&str, u32, u32)],
16905 ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16906 self.ensure_not_poisoned()?;
16907 self.observe_evaluation_resource_request(EvaluationRequestKind::Cells, |engine| {
16908 engine.evaluate_cells_unobserved(targets)
16909 })
16910 }
16911
16912 fn evaluate_cells_unobserved(
16913 &mut self,
16914 targets: &[(&str, u32, u32)],
16915 ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16916 self.observe_function_semantic_epoch()?;
16917 debug_assert!(
16918 !self.graph.deferred_dirty_active(),
16919 "deferred-dirty scope leaked into evaluate_cells: a begin_deferred_dirty \
16920 was not balanced by end_deferred_dirty"
16921 );
16922 self.validate_deterministic_mode()?;
16923 if targets.is_empty() {
16924 return Ok(Vec::new());
16925 }
16926 let typed_targets = self.legacy_coordinate_targets(targets);
16927 self.evaluate_mixed_targets(&typed_targets, None)?;
16928 Ok(targets
16929 .iter()
16930 .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
16931 .collect())
16932 }
16933
16934 pub fn evaluate_cells_cancellable(
16935 &mut self,
16936 targets: &[(&str, u32, u32)],
16937 cancel: crate::engine::CancelToken,
16938 ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16939 self.ensure_not_poisoned()?;
16940 self.observe_evaluation_resource_request(
16941 EvaluationRequestKind::CellsCancellable,
16942 |engine| {
16943 engine.observe_function_semantic_epoch()?;
16944 engine.active_cancel_flag = Some(cancel.clone());
16945 let res = engine.evaluate_cells_cancellable_impl(targets, cancel.as_flag());
16946 engine.active_cancel_flag = None;
16947 res
16948 },
16949 )
16950 }
16951
16952 fn evaluate_cells_cancellable_impl(
16953 &mut self,
16954 targets: &[(&str, u32, u32)],
16955 cancel_flag: &AtomicBool,
16956 ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16957 self.validate_deterministic_mode()?;
16958 if targets.is_empty() {
16959 return Ok(Vec::new());
16960 }
16961 if cancel_flag.load(Ordering::Relaxed) {
16962 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16963 .with_message("Evaluation cancelled before target preparation"));
16964 }
16965 let typed_targets = self.legacy_coordinate_targets(targets);
16966 self.evaluate_mixed_targets(&typed_targets, None)?;
16967 Ok(targets
16968 .iter()
16969 .map(|(sheet, row, col)| self.get_cell_value(sheet, *row, *col))
16970 .collect())
16971 }
16972
16973 pub fn evaluate_cells_with_target_delta(
16974 &mut self,
16975 targets: &[(&str, u32, u32)],
16976 ) -> Result<(Vec<Option<LiteralValue>>, crate::engine::TargetEvalDelta), ExcelError> {
16977 self.ensure_not_poisoned()?;
16978 self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
16979 engine.observe_function_semantic_epoch()?;
16980 engine.validate_deterministic_mode()?;
16981 if targets.is_empty() {
16982 return Ok((Vec::new(), crate::engine::TargetEvalDelta::default()));
16983 }
16984 let mut collector = DeltaCollector::new(DeltaMode::Cells);
16985 engine.evaluate_until_with_delta_collector(targets, &mut collector)?;
16986 let values = targets
16987 .iter()
16988 .map(|(sheet, row, col)| engine.get_cell_value(sheet, *row, *col))
16989 .collect();
16990 Ok((values, collector.finish_target()))
16991 })
16992 }
16993
16994 pub fn evaluate_cells_with_delta(
16995 &mut self,
16996 targets: &[(&str, u32, u32)],
16997 ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
16998 self.ensure_not_poisoned()?;
16999 self.evaluate_cells_with_delta_policy(targets, EvalDeltaCompatibilityPolicy::Unlimited)
17000 }
17001
17002 pub fn evaluate_cells_with_delta_policy(
17003 &mut self,
17004 targets: &[(&str, u32, u32)],
17005 policy: EvalDeltaCompatibilityPolicy,
17006 ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
17007 self.ensure_not_poisoned()?;
17008 self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
17009 engine.evaluate_cells_with_delta_unobserved(targets, policy)
17010 })
17011 }
17012
17013 fn evaluate_cells_with_delta_unobserved(
17014 &mut self,
17015 targets: &[(&str, u32, u32)],
17016 policy: EvalDeltaCompatibilityPolicy,
17017 ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
17018 self.observe_function_semantic_epoch()?;
17019 self.validate_deterministic_mode()?;
17020 if targets.is_empty() {
17021 return Ok((Vec::new(), EvalDelta::default()));
17022 }
17023 let mut collector = DeltaCollector::new(DeltaMode::Cells);
17024 self.evaluate_until_with_delta_collector(targets, &mut collector)?;
17025 let values = targets
17026 .iter()
17027 .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
17028 .collect();
17029 Ok((values, collector.finish_with_policy(policy)?))
17030 }
17031
17032 pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
17034 self.ensure_not_poisoned()?;
17035 if targets.is_empty() {
17036 return Ok(EvalPlan {
17037 total_vertices_to_evaluate: 0,
17038 layers: Vec::new(),
17039 cycles_detected: 0,
17040 dirty_count: 0,
17041 volatile_count: 0,
17042 parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17043 estimated_parallel_layers: 0,
17044 target_cells: Vec::new(),
17045 });
17046 }
17047 if self.config.defer_graph_building && self.has_staged_formulas() {
17048 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
17049 "Evaluation plan requested with deferred graph; build first or call evaluate_*",
17050 ));
17051 }
17052
17053 let addresses: Vec<String> = targets
17055 .iter()
17056 .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
17057 .collect();
17058
17059 let mut target_addrs = Vec::new();
17061 for (sheet, row, col) in targets {
17062 if let Some(sheet_id) = self.graph.sheet_id(sheet) {
17063 let coord = Coord::from_excel(*row, *col, true, true);
17064 target_addrs.push(CellRef::new(sheet_id, coord));
17065 }
17066 }
17067
17068 let mut target_vertex_ids = Vec::new();
17070 for addr in &target_addrs {
17071 if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
17072 target_vertex_ids.push(vertex_id);
17073 }
17074 }
17075
17076 if target_vertex_ids.is_empty() {
17077 return Ok(EvalPlan {
17078 total_vertices_to_evaluate: 0,
17079 layers: Vec::new(),
17080 cycles_detected: 0,
17081 dirty_count: 0,
17082 volatile_count: 0,
17083 parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17084 estimated_parallel_layers: 0,
17085 target_cells: addresses,
17086 });
17087 }
17088
17089 let (precedents_to_eval, vdeps) = self.demand_subgraph(&target_vertex_ids)?;
17091
17092 if precedents_to_eval.is_empty() {
17093 return Ok(EvalPlan {
17094 total_vertices_to_evaluate: 0,
17095 layers: Vec::new(),
17096 cycles_detected: 0,
17097 dirty_count: 0,
17098 volatile_count: 0,
17099 parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17100 estimated_parallel_layers: 0,
17101 target_cells: addresses,
17102 });
17103 }
17104
17105 let mut dirty_count = 0;
17107 let mut volatile_count = 0;
17108 for &vertex_id in &precedents_to_eval {
17109 if self.graph.is_dirty(vertex_id) {
17110 dirty_count += 1;
17111 }
17112 if self.graph.is_volatile(vertex_id) {
17113 volatile_count += 1;
17114 }
17115 }
17116
17117 let schedule = self.create_authority_schedule(&precedents_to_eval, &vdeps, None)?;
17119
17120 let mut layers = Vec::new();
17122 let mut estimated_parallel_layers = 0;
17123 let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
17124
17125 for layer in &schedule.layers {
17126 let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
17127 if parallel_eligible {
17128 estimated_parallel_layers += 1;
17129 }
17130
17131 let sample_cells: Vec<String> = layer
17133 .vertices
17134 .iter()
17135 .take(5)
17136 .filter_map(|&vertex_id| {
17137 self.graph
17138 .get_cell_ref_for_vertex(vertex_id)
17139 .map(|cell_ref| {
17140 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17141 format!(
17142 "{}!{}{}",
17143 sheet_name,
17144 Self::col_to_letters(cell_ref.coord.col().saturating_add(1)),
17145 cell_ref.coord.row() + 1
17146 )
17147 })
17148 })
17149 .collect();
17150
17151 layers.push(LayerInfo {
17152 vertex_count: layer.vertices.len(),
17153 parallel_eligible,
17154 sample_cells,
17155 });
17156 }
17157
17158 Ok(EvalPlan {
17159 total_vertices_to_evaluate: precedents_to_eval.len(),
17160 layers,
17161 cycles_detected: schedule.cycles.len(),
17162 dirty_count,
17163 volatile_count,
17164 parallel_enabled,
17165 estimated_parallel_layers,
17166 target_cells: addresses,
17167 })
17168 }
17169 fn create_evaluation_schedule(
17171 &mut self,
17172 to_evaluate: &[VertexId],
17173 ) -> Result<EvaluationScheduleBuildOutput, ExcelError> {
17174 #[cfg(any(test, feature = "benchmark_internal"))]
17175 {
17176 self.recalc_reuse_probe.get_mut().unwrap().schedule_requests += 1;
17177 }
17178 #[cfg(any(test, feature = "legacy_oracle"))]
17181 self.graph.flush_pending_edge_deltas();
17182 self.graph.authority_sync();
17184 if self.can_use_static_schedule_cache(to_evaluate) {
17185 let revision = self.schedule_cache_authority_revision();
17187 let current = |e: &CachedScheduleEntry| {
17188 e.topology_epoch == self.topology_epoch && e.authority_revision == revision
17189 };
17190 if !self
17191 .cached_static_schedule
17192 .as_ref()
17193 .is_some_and(|c| current(c) && c.candidate_vertices.equals(to_evaluate))
17194 && let Some(i) = self.recent_schedules.iter().position(|e| {
17195 current(e)
17196 && e.candidate_vertices.len() == to_evaluate.len()
17197 && e.candidate_vertices.equals(to_evaluate)
17198 })
17199 {
17200 let hit = self.recent_schedules.remove(i);
17201 if let Some(previous) = self.cached_static_schedule.replace(hit) {
17202 self.retain_recent_schedule(previous);
17203 }
17204 }
17205 if let Some(cached) = self.cached_static_schedule.as_ref()
17206 && cached.topology_epoch == self.topology_epoch
17207 && cached.authority_revision == self.schedule_cache_authority_revision()
17208 && cached.candidate_vertices.equals(to_evaluate)
17209 {
17210 let meta = ScheduleBuildMeta {
17211 candidate_vertices: to_evaluate.len(),
17212 vdeps_vertices: 0,
17213 vdeps_edges: 0,
17214 builder_elapsed_ms: 0,
17215 used_virtual_schedule: false,
17216 schedule_cache_hit: true,
17217 schedule_cache_eligible: true,
17218 };
17219 #[cfg(any(test, feature = "benchmark_internal"))]
17220 {
17221 let mut probe = self.recalc_reuse_probe.lock().unwrap();
17222 probe.schedule_cache_hits += 1;
17223 probe.schedule_shared_handles += 1;
17224 }
17225 return Ok((
17226 EvaluationSchedule::Shared(Arc::clone(&cached.schedule)),
17227 FxHashMap::default(),
17228 meta,
17229 ));
17230 }
17231
17232 let (schedule, vdeps, mut meta) = match self.schedule_from_base(to_evaluate)? {
17233 Some(schedule) => (
17234 schedule,
17235 FxHashMap::default(),
17236 ScheduleBuildMeta {
17237 candidate_vertices: to_evaluate.len(),
17238 vdeps_vertices: 0,
17239 vdeps_edges: 0,
17240 builder_elapsed_ms: 0,
17241 used_virtual_schedule: false,
17242 schedule_cache_hit: false,
17243 schedule_cache_eligible: true,
17244 },
17245 ),
17246 None => self.create_evaluation_schedule_active(to_evaluate)?,
17247 };
17248 meta.schedule_cache_hit = false;
17249 meta.schedule_cache_eligible = true;
17250 #[cfg(any(test, feature = "benchmark_internal"))]
17251 {
17252 self.recalc_reuse_probe
17253 .get_mut()
17254 .unwrap()
17255 .schedule_cache_misses += 1;
17256 }
17257 let schedule = if vdeps.is_empty() {
17258 let mut schedule = schedule;
17261 schedule.units.shrink_to_fit();
17262 for layer in &mut schedule.layers {
17263 layer.vertices.shrink_to_fit();
17264 }
17265 schedule.layers.shrink_to_fit();
17266 for cycle in &mut schedule.cycles {
17267 cycle.shrink_to_fit();
17268 }
17269 schedule.cycles.shrink_to_fit();
17270 let schedule = Arc::new(schedule);
17271 #[cfg(any(test, feature = "benchmark_internal"))]
17272 {
17273 self.recalc_reuse_probe
17274 .get_mut()
17275 .unwrap()
17276 .schedule_shared_handles += 1;
17277 }
17278 let entry = CachedScheduleEntry {
17279 topology_epoch: self.topology_epoch,
17280 authority_revision: self.schedule_cache_authority_revision(),
17281 candidate_vertices: VertexIdRuns::from_slice(to_evaluate),
17282 schedule: Arc::clone(&schedule),
17283 };
17284 if let Some(previous) = self.cached_static_schedule.replace(entry) {
17285 self.retain_recent_schedule(previous);
17286 }
17287 EvaluationSchedule::Shared(schedule)
17288 } else {
17289 EvaluationSchedule::Owned(schedule)
17290 };
17291 return Ok((schedule, vdeps, meta));
17292 }
17293
17294 let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_active(to_evaluate)?;
17295 meta.schedule_cache_hit = false;
17296 meta.schedule_cache_eligible = false;
17297 #[cfg(any(test, feature = "benchmark_internal"))]
17298 {
17299 self.recalc_reuse_probe
17300 .get_mut()
17301 .unwrap()
17302 .schedule_cache_ineligible += 1;
17303 }
17304 Ok((EvaluationSchedule::Owned(schedule), vdeps, meta))
17305 }
17306
17307 fn schedule_from_base(
17313 &mut self,
17314 to_evaluate: &[VertexId],
17315 ) -> Result<Option<crate::engine::scheduler::Schedule>, ExcelError> {
17316 let revision = self.schedule_cache_authority_revision();
17317 let current = |e: &&CachedScheduleEntry| {
17318 e.topology_epoch == self.topology_epoch && e.authority_revision == revision
17319 };
17320 let Some(base) = [
17323 self.base_schedule.as_ref(),
17324 self.cached_static_schedule.as_ref(),
17325 ]
17326 .into_iter()
17327 .flatten()
17328 .filter(current)
17329 .max_by_key(|e| e.candidate_vertices.len()) else {
17330 return Ok(None);
17331 };
17332 let base_len = base.candidate_vertices.len();
17333 if base_len <= BASE_SCHEDULE_MIN_VERTICES
17334 || to_evaluate.len() < BASE_SCHEDULE_MIN_REQUEST
17335 || to_evaluate.len() > base_len
17336 || base_len > to_evaluate.len().saturating_mul(BASE_SCHEDULE_RATIO)
17337 {
17338 return Ok(None);
17339 }
17340 let mut keep = crate::engine::idset::DenseIdSet::default();
17341 keep.extend(to_evaluate.iter().copied());
17342 let Some((schedule, kept)) = base.schedule.restrict(&keep) else {
17343 return Ok(None);
17344 };
17345 if kept != keep.len() {
17347 return Ok(None);
17348 }
17349 if let Some(ledger) = self.active_resource_ledger.as_mut() {
17350 let layers: usize = schedule
17351 .layers
17352 .iter()
17353 .map(|l| {
17354 l.vertices.capacity() * std::mem::size_of::<VertexId>()
17355 + l.runs.capacity()
17356 * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
17357 })
17358 .sum();
17359 let bytes = (keep.heap_bytes()
17360 + layers
17361 + schedule.layers.capacity()
17362 * std::mem::size_of::<crate::engine::scheduler::Layer>()
17363 + schedule.units.capacity()
17364 * std::mem::size_of::<crate::engine::scheduler::ScheduleUnit>()
17365 + schedule
17366 .cycles
17367 .iter()
17368 .map(|c| c.capacity() * 4)
17369 .sum::<usize>()) as u64;
17370 ledger
17371 .reserve_schedule_discovery(bytes)
17372 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17373 ledger
17374 .release_scratch(bytes)
17375 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17376 }
17377 #[cfg(debug_assertions)]
17378 self.debug_check_restricted_schedule(to_evaluate, &schedule);
17379 #[cfg(any(test, feature = "benchmark_internal"))]
17380 {
17381 self.recalc_reuse_probe
17382 .get_mut()
17383 .unwrap()
17384 .schedule_base_restrictions += 1;
17385 }
17386 Ok(Some(schedule))
17387 }
17388
17389 #[cfg(debug_assertions)]
17393 fn debug_check_restricted_schedule(
17394 &self,
17395 to_evaluate: &[VertexId],
17396 schedule: &crate::engine::scheduler::Schedule,
17397 ) {
17398 let mut position: FxHashMap<VertexId, usize> = FxHashMap::default();
17399 for (u, unit) in schedule.units.iter().enumerate() {
17400 let vertices: &[VertexId] = match *unit {
17401 crate::engine::scheduler::ScheduleUnit::Layer(i) => {
17402 &schedule.unit_layer(i).vertices
17403 }
17404 crate::engine::scheduler::ScheduleUnit::Cycle(i) => schedule.unit_cycle(i),
17405 };
17406 for &v in vertices {
17407 assert!(
17408 position.insert(v, u).is_none(),
17409 "vertex twice in a restricted schedule"
17410 );
17411 }
17412 }
17413 assert_eq!(position.len(), to_evaluate.len());
17414 if to_evaluate.len() > 512 {
17415 return;
17416 }
17417 let Ok(store) = self.graph.authority_plan_store() else {
17420 return;
17421 };
17422 let cells: Vec<(VertexId, (u16, u32, u32))> = to_evaluate
17423 .iter()
17424 .filter_map(|&v| self.graph.authority_cell_of_vertex(v).map(|c| (v, c)))
17425 .collect();
17426 for &(v, (sheet, row, col)) in &cells {
17427 let Some(owner) = store.owner_at((sheet, row, col)) else {
17428 continue;
17429 };
17430 let Ok(refined) = store.refine_owner_column(owner, col, row, row, None) else {
17431 continue;
17432 };
17433 for piece in &refined.pieces {
17434 for edge in &refined.edges[piece.edge_start..piece.edge_end] {
17435 let Some(image) = edge.proj.forward(&piece.domain) else {
17436 continue;
17437 };
17438 for &(d, (s2, r2, c2)) in &cells {
17439 if d == v
17440 || edge.proj.sheet != s2
17441 || !(image.r0 <= r2
17442 && r2 <= image.r1
17443 && image.c0 <= c2
17444 && c2 <= image.c1)
17445 {
17446 continue;
17447 }
17448 let (pd, pv) = (position[&d], position[&v]);
17449 let same_ok = pd == pv
17450 && match schedule.units[pv] {
17451 crate::engine::scheduler::ScheduleUnit::Layer(i) => {
17452 schedule.unit_layer(i).sequential
17453 }
17454 crate::engine::scheduler::ScheduleUnit::Cycle(_) => true,
17455 };
17456 assert!(
17457 pd < pv || same_ok,
17458 "restricted schedule orders {v:?} before its precedent {d:?}"
17459 );
17460 }
17461 }
17462 }
17463 }
17464 }
17465
17466 fn maybe_compress_formulas(&mut self) {
17470 if !self.config.formula_compression {
17471 return;
17472 }
17473 let builds = self.graph.authority_host().builds;
17474 if self.compressed_at_build == Some(builds) {
17475 return;
17476 }
17477 self.compressed_at_build = Some(builds);
17478 if self.has_staged_formulas() {
17479 self.graph.virtualize_family_members();
17482 return;
17483 }
17484 let pool = self.thread_pool.clone();
17485 let (_, garbage) = self.graph.compress_family_formulas(pool.as_deref());
17486 self.graph.virtualize_family_members();
17487 match pool {
17491 Some(pool) if garbage.len() >= 1024 => pool.spawn(move || drop(garbage)),
17492 _ => drop(garbage),
17493 }
17494 }
17495
17496 fn create_evaluation_schedule_active(
17497 &mut self,
17498 to_evaluate: &[VertexId],
17499 ) -> Result<ScheduleBuildOutput, ExcelError> {
17500 self.graph.authority_sync();
17501 self.maybe_compress_formulas();
17502 let mut ledger = self.active_resource_ledger.take();
17503 let result = self.create_evaluation_schedule_uncached(to_evaluate, ledger.as_mut());
17504 self.active_resource_ledger = ledger;
17505 result
17506 }
17507
17508 fn create_evaluation_schedule_uncached(
17509 &self,
17510 to_evaluate: &[VertexId],
17511 #[allow(unused_variables)] ledger: Option<&mut ResourceLedger>,
17512 ) -> Result<ScheduleBuildOutput, ExcelError> {
17513 #[cfg(any(test, feature = "benchmark_internal"))]
17514 {
17515 self.recalc_reuse_probe.lock().unwrap().schedule_builds += 1;
17516 }
17517 let builder = VirtualDepBuilder::new(self);
17518 #[allow(unused_mut)]
17519 let (mut vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
17520 if self.config.enable_virtual_dep_telemetry {
17521 let build_started = crate::instant::FzInstant::now();
17522 let (vdeps, augmented) = builder.build(to_evaluate);
17523 let builder_elapsed_ms = build_started.elapsed().as_millis();
17524 let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
17525 (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
17526 } else {
17527 let (vdeps, augmented) = builder.build(to_evaluate);
17528 (vdeps, augmented, 0, 0)
17529 };
17530
17531 {
17533 self.freshness_merge_hints(to_evaluate, &mut vdeps);
17534 self.freshness_extent_hints(to_evaluate, &mut vdeps);
17535 }
17536 let mut final_evaluate = to_evaluate.to_vec();
17537 if !augmented.is_empty() {
17538 final_evaluate.extend(augmented);
17539 final_evaluate.sort_unstable();
17540 final_evaluate.dedup();
17541 }
17542
17543 let use_virtual = !vdeps.is_empty();
17544
17545 let schedule = self.create_authority_schedule(&final_evaluate, &vdeps, ledger)?;
17546
17547 let meta = ScheduleBuildMeta {
17548 candidate_vertices: to_evaluate.len(),
17549 vdeps_vertices: vdeps.len(),
17550 vdeps_edges,
17551 builder_elapsed_ms,
17552 used_virtual_schedule: use_virtual,
17553 schedule_cache_hit: false,
17554 schedule_cache_eligible: false,
17555 };
17556
17557 Ok((schedule, vdeps, meta))
17558 }
17559
17560 fn create_authority_schedule(
17561 &self,
17562 candidates: &[VertexId],
17563 vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
17564 mut ledger: Option<&mut ResourceLedger>,
17565 ) -> Result<crate::engine::scheduler::Schedule, ExcelError> {
17566 use crate::engine::authority::{
17567 geom::{Cover, Rect},
17568 plan_schedule, planner,
17569 proj::{AxisMap, RefProj},
17570 store::{EdgeKey, Tag},
17571 };
17572 let failure = |message: String| {
17573 ExcelError::new(ExcelErrorKind::Error)
17574 .with_message(format!("unified_authority planner: {message}"))
17575 };
17576 self.cancellation_checkpoint("Evaluation cancelled before authority planning")?;
17577 let store = self
17578 .graph
17579 .authority_plan_store()
17580 .map_err(Self::authority_excel_error)?;
17581 if let [only] = candidates
17584 && vdeps.is_empty()
17585 && self.graph.authority_host().observed(*only).is_none()
17586 && let Some(cell) = self.graph.authority_cell_of_vertex(*only)
17587 && cell.0 != crate::engine::authority::geom::SYMBOL_SHEET
17588 && let Some(single) = planner::plan_single(store, cell)
17589 {
17590 #[cfg(debug_assertions)]
17591 {
17592 let mut cover = Cover::new();
17593 cover.insert_rect(cell.0, &Rect::new(cell.1, cell.2, cell.1, cell.2));
17594 let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
17595 .expect("general plan of one cell");
17596 assert_eq!(
17597 general.cells.as_slice(),
17598 &[single],
17599 "single-cell plan differs from the planner at {cell:?}"
17600 );
17601 }
17602 let adapted = plan_schedule::schedule(
17603 &[single],
17604 0,
17605 None,
17606 |cell| {
17607 self.graph
17608 .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17609 .ok_or_else(|| failure("missing executor identity".to_owned()))
17610 },
17611 |_work| Ok(()),
17612 )
17613 .map_err(|error| match error {
17614 plan_schedule::ScheduleError::Runtime(error) => error,
17615 other => failure(format!("{other:?}")),
17616 })?;
17617 if let Some(ledger) = ledger {
17618 ledger
17619 .reserve_schedule_discovery(adapted.peak_heap_bytes)
17620 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17621 ledger
17622 .release_scratch(adapted.peak_heap_bytes)
17623 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17624 }
17625 return Ok(adapted.schedule);
17626 }
17627 if (2..=planner::SMALL_PLAN_MAX).contains(&candidates.len())
17630 && vdeps.is_empty()
17631 && candidates
17632 .iter()
17633 .all(|&v| self.graph.authority_host().observed(v).is_none())
17634 {
17635 let cells: Option<Vec<(u16, u32, u32)>> = candidates
17636 .iter()
17637 .map(|&v| self.graph.authority_cell_of_vertex(v))
17638 .collect();
17639 if let Some(small) = cells.as_deref().and_then(|c| planner::plan_small(store, c)) {
17640 #[cfg(debug_assertions)]
17641 {
17642 let mut cover = Cover::new();
17643 for c in cells.as_deref().unwrap_or_default() {
17644 cover.insert_rect(c.0, &Rect::new(c.1, c.2, c.1, c.2));
17645 }
17646 let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
17647 .expect("general plan of a small request");
17648 let key = |c: &planner::OrderedCell| (c.sheet, c.row, c.col, c.id, c.owner);
17649 let mut a: Vec<_> = general.cells.iter().map(key).collect();
17650 let mut b: Vec<_> = small.iter().map(key).collect();
17651 a.sort_unstable();
17652 b.sort_unstable();
17653 assert_eq!(a, b, "small plan cells differ from the planner");
17654 assert!(
17655 general.cells.iter().all(|c| c.cycle.is_none()),
17656 "small plan of a cyclic request"
17657 );
17658 }
17659 let adapted = plan_schedule::schedule(
17660 &small,
17661 0,
17662 None,
17663 |cell| {
17664 self.graph
17665 .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17666 .ok_or_else(|| failure("missing executor identity".to_owned()))
17667 },
17668 |_work| Ok(()),
17669 )
17670 .map_err(|error| match error {
17671 plan_schedule::ScheduleError::Runtime(error) => error,
17672 other => failure(format!("{other:?}")),
17673 })?;
17674 if let Some(ledger) = ledger {
17675 ledger
17676 .reserve_schedule_discovery(adapted.peak_heap_bytes)
17677 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17678 ledger
17679 .release_scratch(adapted.peak_heap_bytes)
17680 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17681 }
17682 return Ok(adapted.schedule);
17683 }
17684 }
17685 let mut cover = Cover::new();
17690 {
17691 let cell_of = |&id: &VertexId| {
17692 self.graph
17693 .authority_cell_of_vertex(id)
17694 .map(|(sheet, row, col)| (sheet, col, row))
17695 };
17696 let cells: Vec<(u16, u32, u32)> = match self.thread_pool.as_deref() {
17699 Some(pool) if candidates.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
17700 use rayon::prelude::*;
17701 pool.install(|| {
17702 let mut cells: Vec<_> = candidates.par_iter().filter_map(cell_of).collect();
17703 cells.par_sort_unstable();
17704 cells
17705 })
17706 }
17707 _ => {
17708 let mut cells: Vec<_> = candidates.iter().filter_map(cell_of).collect();
17709 cells.sort_unstable();
17710 cells
17711 }
17712 };
17713 let mut i = 0;
17714 while i < cells.len() {
17715 let (sheet, col, r0) = cells[i];
17716 let mut r1 = r0;
17717 let mut j = i + 1;
17718 while j < cells.len() && cells[j].0 == sheet && cells[j].1 == col {
17719 if cells[j].2 > r1 + 1 {
17720 break;
17721 }
17722 r1 = r1.max(cells[j].2);
17723 j += 1;
17724 }
17725 cover.insert_rect(sheet, &Rect::new(r0, col, r1, col));
17726 i = j;
17727 }
17728 }
17729 let mut hints = Vec::new();
17730 for (&reader, deps) in vdeps {
17731 let Some(reader) = self.graph.authority_cell_of_vertex(reader) else {
17732 continue;
17733 };
17734 for &dependency in deps {
17735 let Some(dep) = self.graph.authority_cell_of_vertex(dependency) else {
17736 continue;
17737 };
17738 hints.push(planner::PlanHint {
17739 reader: (reader.0, reader.2, reader.1),
17740 edge: EdgeKey {
17741 dep_sheet: reader.0,
17742 tag: Tag::X,
17743 lk: u32::MAX,
17744 proj: RefProj {
17745 sheet: dep.0,
17746 rows: AxisMap::fixed(dep.1, dep.1),
17747 cols: AxisMap::fixed(dep.2, dep.2),
17748 },
17749 },
17750 });
17751 }
17752 }
17753 let host = self.graph.authority_host();
17755 for &id in candidates.iter().filter(|_| host.has_observed()) {
17756 let Some(reads) = host.observed(id) else {
17757 continue;
17758 };
17759 let Some(reader) = self.graph.authority_cell_of_vertex(id) else {
17760 continue;
17761 };
17762 for &(sheet, r0, c0, r1, c1) in reads {
17763 hints.push(planner::PlanHint {
17764 reader: (reader.0, reader.2, reader.1),
17765 edge: EdgeKey {
17766 dep_sheet: reader.0,
17767 tag: Tag::X,
17768 lk: u32::MAX,
17769 proj: RefProj {
17770 sheet,
17771 rows: AxisMap::fixed(r0, r1),
17772 cols: AxisMap::fixed(c0, c1),
17773 },
17774 },
17775 });
17776 }
17777 }
17778 hints.sort_unstable_by_key(|hint| hint.reader);
17779 let checkpoint = ledger
17780 .as_ref()
17781 .map_or(0, |ledger| ledger.scratch_checkpoint());
17782 let scratch_limit = ledger
17783 .as_ref()
17784 .and_then(|ledger| ledger.schedule_discovery_limit())
17785 .map(|limit| limit.saturating_sub(checkpoint));
17786 let ordered = planner::plan_with_hints(store, &cover, &hints, scratch_limit, None, None)
17787 .map_err(|error| failure(format!("{error:?}")))?;
17788 let adapted = plan_schedule::schedule(
17792 &ordered.cells,
17793 ordered.heap_bytes(),
17794 scratch_limit,
17795 |cell| {
17796 self.graph
17797 .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17798 .ok_or_else(|| failure("missing executor identity".to_owned()))
17799 },
17800 |_work| {
17801 self.cancellation_checkpoint("Evaluation cancelled during authority planning")?;
17802 if let Some(ledger) = ledger.as_deref_mut() {
17803 ledger
17804 .checkpoint_deadline()
17805 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17806 }
17807 Ok(())
17808 },
17809 )
17810 .map_err(|error| match error {
17811 plan_schedule::ScheduleError::Runtime(error) => error,
17812 other => failure(format!("{other:?}")),
17813 })?;
17814 if let Some(ledger) = ledger {
17815 let peak = ordered.peak_heap_bytes.max(adapted.peak_heap_bytes);
17816 ledger
17817 .reserve_schedule_discovery(peak)
17818 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17819 ledger
17820 .release_scratch(peak)
17821 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17822 }
17823 match self.thread_pool.as_deref() {
17825 Some(pool) if ordered.cells.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
17826 pool.spawn(move || drop(ordered))
17827 }
17828 _ => drop(ordered),
17829 }
17830 Ok(adapted.schedule)
17831 }
17832
17833 fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
17839 {
17840 let host = self.graph.authority_host();
17844 !to_evaluate.is_empty()
17845 && !self.freshness_has_hints()
17846 && to_evaluate
17847 .iter()
17848 .all(|&v| !self.graph.is_dynamic(v) || host.observed(v).is_some())
17849 }
17850 }
17851
17852 fn schedule_cache_authority_revision(&self) -> (u64, u64) {
17853 {
17854 let host = self.graph.authority_host();
17857 (host.revision(), host.rev_dyn())
17858 }
17859 }
17860
17861 fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
17862 VirtualDepTelemetry {
17863 fallback_mode_activations: self.virtual_dep_fallback_activations,
17864 ..VirtualDepTelemetry::default()
17865 }
17866 }
17867
17868 fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
17869 telemetry.candidate_vertices_total += meta.candidate_vertices;
17870 telemetry.vdeps_vertices_total += meta.vdeps_vertices;
17871 telemetry.vdeps_edges_total += meta.vdeps_edges;
17872 telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
17873 if meta.schedule_cache_eligible {
17874 if meta.schedule_cache_hit {
17875 telemetry.schedule_cache_hits += 1;
17876 telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
17877 } else {
17878 telemetry.schedule_cache_misses += 1;
17879 }
17880 }
17881 if meta.used_virtual_schedule {
17882 telemetry.schedule_virtual_passes += 1;
17883 } else {
17884 telemetry.schedule_static_passes += 1;
17885 }
17886 }
17887
17888 fn finish_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
17893 self.finish_pass_dirty_scoped(to_evaluate, changed, true)
17894 }
17895
17896 fn finish_target_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
17899 self.finish_pass_dirty_scoped(to_evaluate, changed, false)
17900 }
17901
17902 fn finish_pass_dirty_scoped(
17903 &mut self,
17904 to_evaluate: &[VertexId],
17905 changed: &[VertexId],
17906 #[allow(unused_variables)] whole_workbook: bool,
17907 ) -> bool {
17908 self.freshness_finish_pass(to_evaluate, changed, whole_workbook)
17909 }
17910
17911 fn begin_pass(
17913 &mut self,
17914 #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
17915 ) {
17916 self.freshness_begin_pass(schedule);
17917 }
17918
17919 fn stop_after_unit(
17921 &mut self,
17922 #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
17923 #[allow(unused_variables)] index: usize,
17924 ) -> bool {
17925 self.freshness_stop_after_unit(schedule, index)
17926 }
17927
17928 fn changed_virtual_dep_vertices(
17929 &mut self,
17930 to_evaluate: &[VertexId],
17931 old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
17932 ) -> Vec<VertexId> {
17933 #[cfg(test)]
17934 if self.force_virtual_dep_changes_remaining_for_test > 0
17935 && let Some(vertex) = to_evaluate.first().copied()
17936 {
17937 self.force_virtual_dep_changes_remaining_for_test -= 1;
17938 return vec![vertex];
17939 }
17940 if self.freshness_armed() {
17943 return Vec::new();
17944 }
17945 if !to_evaluate
17946 .iter()
17947 .copied()
17948 .any(|v| self.graph.is_dynamic(v))
17949 {
17950 return Vec::new();
17951 }
17952
17953 let builder = VirtualDepBuilder::new(self);
17954 let (new_vdeps, _) = builder.build(to_evaluate);
17955
17956 let mut candidates = FxHashSet::default();
17957 candidates.extend(old_vdeps.keys().copied());
17958 candidates.extend(new_vdeps.keys().copied());
17959
17960 let mut changed = Vec::new();
17961 for v in candidates {
17962 if old_vdeps.get(&v) != new_vdeps.get(&v) {
17963 changed.push(v);
17964 }
17965 }
17966 changed
17967 }
17968
17969 #[allow(clippy::type_complexity)]
17975 fn demand_subgraph(
17976 &self,
17977 targets: &[VertexId],
17978 ) -> Result<
17979 (
17980 Vec<VertexId>,
17981 rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
17982 ),
17983 ExcelError,
17984 > {
17985 self.authority_demand_subgraph(targets)
17986 }
17987
17988 #[allow(clippy::type_complexity)]
17994 fn authority_demand_subgraph(
17995 &self,
17996 targets: &[VertexId],
17997 ) -> Result<
17998 (
17999 Vec<VertexId>,
18000 rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
18001 ),
18002 ExcelError,
18003 > {
18004 use crate::engine::authority::geom::{Cell, Rect, SYMBOL_SHEET};
18005 use crate::engine::authority::store::TagFilter;
18006 use rustc_hash::{FxHashMap, FxHashSet};
18007 let store = self
18008 .graph
18009 .authority_plan_store()
18010 .map_err(Self::authority_excel_error)?;
18011 let ids = store.ids();
18012 let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
18013 let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
18014 let mut visited: FxHashSet<Cell> = FxHashSet::default();
18015 let mut stack: Vec<(Cell, u32)> = Vec::new();
18018 let push_formulas = |stack: &mut Vec<(Cell, u32)>,
18020 visited: &FxHashSet<Cell>,
18021 sheet: u16,
18022 rect: Rect| {
18023 for col in rect.c0..=rect.c1 {
18024 ids.visit_runs_in(sheet, col, rect.r0, rect.r1, &mut |h| {
18025 let run = ids.run(h);
18026 let r0 = run.row_start.max(rect.r0);
18027 let r1 = (run.row_start + run.len - 1).min(rect.r1);
18028 for row in r0..=r1 {
18029 if !visited.contains(&(sheet, row, col)) {
18030 stack.push(((sheet, row, col), run.first_id + (row - run.row_start)));
18031 }
18032 }
18033 });
18034 }
18035 };
18036 for &v in targets {
18037 if let Some(table) = self.graph.table_by_vertex(v) {
18038 let (s, e) = (table.range.start, table.range.end);
18042 push_formulas(
18043 &mut stack,
18044 &visited,
18045 s.sheet_id,
18046 Rect::new(s.coord.row(), s.coord.col(), e.coord.row(), e.coord.col()),
18047 );
18048 } else if let Some(cell) = self.graph.authority_cell_of_vertex(v) {
18049 stack.push((cell, crate::engine::authority::identity::NO_VID));
18050 }
18051 }
18052 let mut hits = Vec::new();
18053 #[cfg(any(test, feature = "benchmark_internal"))]
18054 let (mut probe_vertices, mut probe_clean_formulas, mut probe_edges, mut probe_dynamic) =
18055 (0, 0, 0, 0);
18056 while let Some((cell, id)) = stack.pop() {
18057 if !visited.insert(cell) {
18058 continue;
18059 }
18060 let Some(v) = self.graph.authority_vertex_of_formula(id, cell) else {
18061 continue;
18062 };
18063 if !self.graph.vertex_exists(v) {
18064 continue;
18065 }
18066 #[cfg(any(test, feature = "benchmark_internal"))]
18067 {
18068 probe_vertices += 1;
18069 }
18070 match self.graph.get_vertex_kind(v) {
18071 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18072 if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
18073 to_evaluate.insert(v);
18074 } else {
18075 #[cfg(any(test, feature = "benchmark_internal"))]
18076 {
18077 probe_clean_formulas += 1;
18078 }
18079 }
18080 }
18081 VertexKind::NamedScalar | VertexKind::NamedArray => {
18082 to_evaluate.insert(v);
18083 }
18084 _ => {}
18085 }
18086 hits.clear();
18087 store.direct_precedents(cell, TagFilter::All, &mut hits);
18088 #[cfg(any(test, feature = "benchmark_internal"))]
18089 {
18090 probe_edges += hits.len();
18091 }
18092 for &(_, sheet, rect) in &hits {
18093 if sheet != SYMBOL_SHEET {
18096 for anchor in self
18097 .graph
18098 .spill_anchors_in_region(sheet, rect.r0, rect.c0, rect.r1, rect.c1)
18099 {
18100 if anchor != v && self.graph.is_dirty(anchor) {
18101 vdeps.entry(v).or_default().push(anchor);
18102 if let Some(c) = self.graph.authority_cell_of_vertex(anchor) {
18103 stack.push((c, crate::engine::authority::identity::NO_VID));
18104 }
18105 }
18106 }
18107 }
18108 if sheet == SYMBOL_SHEET {
18109 stack.extend((rect.r0..=rect.r1).map(|slot| {
18110 (
18111 (SYMBOL_SHEET, slot, 0),
18112 crate::engine::authority::identity::NO_VID,
18113 )
18114 }));
18115 continue;
18116 }
18117 push_formulas(&mut stack, &visited, sheet, rect);
18118 }
18119 if self.graph.is_dynamic(v) {
18120 #[cfg(any(test, feature = "benchmark_internal"))]
18121 {
18122 probe_dynamic += 1;
18123 }
18124 if let Some(reads) = self.graph.authority_host().observed(v) {
18126 for &(sheet, r0, c0, r1, c1) in reads {
18127 push_formulas(&mut stack, &visited, sheet, Rect::new(r0, c0, r1, c1));
18128 }
18129 }
18130 let (vdeps_map, _) = VirtualDepBuilder::new(self).build(&[v]);
18131 let hinted = self.freshness_hints(v).unwrap_or(&[]);
18134 if let Some(deps) = vdeps_map
18135 .get(&v)
18136 .map(|deps| deps.iter().chain(hinted))
18137 .or(Some([].iter().chain(hinted)))
18138 {
18139 for &u in deps {
18140 vdeps.entry(v).or_default().push(u);
18141 if let Some(c) = self.graph.authority_cell_of_vertex(u) {
18142 stack.push((c, crate::engine::authority::identity::NO_VID));
18143 }
18144 }
18145 }
18146 }
18147 }
18148 let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
18149 result.sort_unstable();
18150 for deps in vdeps.values_mut() {
18151 deps.sort_unstable();
18152 deps.dedup();
18153 }
18154 #[cfg(any(test, feature = "benchmark_internal"))]
18155 {
18156 let mut probe = self.recalc_reuse_probe.lock().unwrap();
18157 probe.demand_builds += 1;
18158 probe.demand_vertices += probe_vertices;
18159 probe.demand_clean_formulas += probe_clean_formulas;
18160 probe.demand_explicit_edges += probe_edges;
18161 probe.demand_virtual_builder_calls += probe_dynamic;
18162 }
18163 Ok((result, vdeps))
18164 }
18165
18166 fn col_to_letters(col: u32) -> String {
18168 col_letters_from_1based(col).expect("column index must be >= 1")
18169 }
18170
18171 pub fn evaluate_all_cancellable(
18173 &mut self,
18174 cancel: crate::engine::CancelToken,
18175 ) -> Result<EvalResult, ExcelError> {
18176 self.ensure_not_poisoned()?;
18177 self.observe_evaluation_resource_request(EvaluationRequestKind::FullCancellable, |engine| {
18178 engine.observe_function_semantic_epoch()?;
18179 engine.active_cancel_flag = Some(cancel.clone());
18180 let res = engine.evaluate_all_cancellable_impl(cancel.as_flag());
18181 engine.active_cancel_flag = None;
18182 res
18183 })
18184 }
18185
18186 fn evaluate_all_cancellable_impl(
18187 &mut self,
18188 cancel_flag: &AtomicBool,
18189 ) -> Result<EvalResult, ExcelError> {
18190 let _source_cache = self.source_cache_session();
18191 self.validate_deterministic_mode()?;
18192 if self.config.defer_graph_building {
18193 self.build_graph_all()?;
18194 }
18195 if cancel_flag.load(Ordering::Relaxed) {
18196 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18197 .with_message("Evaluation cancelled before scheduling".to_string()));
18198 }
18199 self.require_unified_authority()?;
18200 self.begin_evaluation_request();
18201 self.reset_virtual_dep_telemetry_if_disabled();
18202 let start = crate::instant::FzInstant::now();
18203 let mut computed_vertices = 0;
18204 let mut cycle_errors = 0;
18205
18206 let mut replan_iterations = 0;
18207 const MAX_REPLAN: usize = 5;
18208 let mut telemetry = self
18209 .config
18210 .enable_virtual_dep_telemetry
18211 .then(|| self.start_virtual_dep_telemetry());
18212
18213 loop {
18214 if cancel_flag.load(Ordering::Relaxed) {
18215 if let Some(mut t) = telemetry {
18216 t.bailout_reason = Some("cancelled");
18217 t.replan_iterations = replan_iterations;
18218 self.last_virtual_dep_telemetry = t;
18219 }
18220 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18221 .with_message("Evaluation cancelled before scheduling".to_string()));
18222 }
18223
18224 let to_evaluate = self.graph.get_evaluation_vertices();
18225 if to_evaluate.is_empty() {
18226 if let Some(t) = telemetry.as_mut()
18227 && t.bailout_reason.is_none()
18228 {
18229 t.bailout_reason = Some("no_work");
18230 }
18231 break;
18232 }
18233
18234 let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
18235 if let Some(t) = telemetry.as_mut() {
18236 Self::accumulate_schedule_meta(t, &meta);
18237 }
18238
18239 self.begin_pass(&schedule);
18242 for (unit_index, &unit) in schedule.units.iter().enumerate() {
18243 match unit {
18244 ScheduleUnit::Cycle(i) => {
18245 if cancel_flag.load(Ordering::Relaxed) {
18247 if let Some(mut t) = telemetry {
18248 t.bailout_reason = Some("cancelled");
18249 t.replan_iterations = replan_iterations;
18250 self.last_virtual_dep_telemetry = t;
18251 }
18252 return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
18253 "Evaluation cancelled during cycle handling".to_string(),
18254 ));
18255 }
18256
18257 if self.handle_cycle_unit(
18258 schedule.unit_cycle(i),
18259 None,
18260 None,
18261 Some(cancel_flag),
18262 )? > 0
18263 {
18264 cycle_errors += 1;
18265 }
18266 }
18267 ScheduleUnit::Layer(i) => {
18268 let layer = schedule.unit_layer(i);
18269 if cancel_flag.load(Ordering::Relaxed) {
18271 if let Some(mut t) = telemetry {
18272 t.bailout_reason = Some("cancelled");
18273 t.replan_iterations = replan_iterations;
18274 self.last_virtual_dep_telemetry = t;
18275 }
18276 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18277 .with_message("Evaluation cancelled between layers".to_string()));
18278 }
18279
18280 if self.thread_pool.is_some() && layer.vertices.len() > 1 {
18282 computed_vertices +=
18283 self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
18284 } else {
18285 computed_vertices +=
18286 self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
18287 }
18288 }
18289 }
18290 if self.stop_after_unit(&schedule, unit_index) {
18291 break;
18292 }
18293 }
18294
18295 let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
18296 if let Some(t) = telemetry.as_mut() {
18297 t.changed_vdeps_total += changed_vertices.len();
18298 }
18299 self.resource_checkpoint(0)?;
18300 if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
18301 if let Some(t) = telemetry.as_mut() {
18302 t.bailout_reason = Some("converged");
18303 }
18304 break;
18305 }
18306 if replan_iterations >= MAX_REPLAN {
18307 if let Some(mut t) = telemetry.take() {
18308 t.bailout_reason = Some("max_replan");
18309 t.replan_iterations = replan_iterations;
18310 self.last_virtual_dep_telemetry = t;
18311 }
18312 return Err(
18313 self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
18314 );
18315 }
18316 replan_iterations += 1;
18317 }
18318
18319 if let Some(mut t) = telemetry {
18320 t.replan_iterations = replan_iterations;
18321 self.last_virtual_dep_telemetry = t;
18322 }
18323
18324 self.redirty_for_next_recalc();
18326 self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
18327
18328 Ok(EvalResult {
18329 computed_vertices,
18330 cycle_errors,
18331 elapsed: start.elapsed(),
18332 })
18333 }
18334
18335 pub fn evaluate_until_cancellable(
18337 &mut self,
18338 targets: &[&str],
18339 cancel: crate::engine::CancelToken,
18340 ) -> Result<EvalResult, ExcelError> {
18341 self.ensure_not_poisoned()?;
18342 self.observe_evaluation_resource_request(
18343 EvaluationRequestKind::TargetedCancellable,
18344 |engine| {
18345 engine.observe_function_semantic_epoch()?;
18346 engine.active_cancel_flag = Some(cancel.clone());
18347 let res = engine.evaluate_until_cancellable_impl(targets, cancel.as_flag());
18348 engine.active_cancel_flag = None;
18349 res
18350 },
18351 )
18352 }
18353
18354 fn evaluate_until_cancellable_impl(
18355 &mut self,
18356 targets: &[&str],
18357 cancel_flag: &AtomicBool,
18358 ) -> Result<EvalResult, ExcelError> {
18359 if cancel_flag.load(Ordering::Relaxed) {
18360 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18361 .with_message("Evaluation cancelled before target preparation"));
18362 }
18363 let mut typed_targets = Vec::with_capacity(targets.len());
18364 for target in targets {
18365 let (sheet, row, col) = self.parse_a1_notation(target)?;
18366 self.graph.sheet_id_mut(&sheet);
18367 typed_targets.push(crate::engine::EvaluationTarget::Cell { sheet, row, col });
18368 }
18369 self.evaluate_mixed_targets(&typed_targets, None)
18370 }
18371
18372 fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
18373 let mut quoted = false;
18374 let mut separator = None;
18375 let bytes = address.as_bytes();
18376 let mut index = 0usize;
18377 while index < bytes.len() {
18378 match bytes[index] {
18379 b'\'' => {
18380 if quoted && bytes.get(index + 1) == Some(&b'\'') {
18381 index = index.saturating_add(1);
18382 } else {
18383 quoted = !quoted;
18384 }
18385 }
18386 b'!' if !quoted => separator = Some(index),
18387 _ => {}
18388 }
18389 index = index.saturating_add(1);
18390 }
18391 if quoted {
18392 return Err(ExcelError::new(ExcelErrorKind::Ref)
18393 .with_message(format!("Invalid quoted sheet reference `{address}`")));
18394 }
18395 let (sheet, cell_part) = match separator {
18396 Some(separator) => {
18397 let raw_sheet = &address[..separator];
18398 let sheet = if raw_sheet.starts_with('\'') && raw_sheet.ends_with('\'') {
18399 raw_sheet[1..raw_sheet.len().saturating_sub(1)].replace("''", "'")
18400 } else {
18401 raw_sheet.to_string()
18402 };
18403 (sheet, &address[separator + 1..])
18404 }
18405 None => (self.default_sheet_name().to_string(), address),
18406 };
18407
18408 let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
18409 ExcelError::new(ExcelErrorKind::Ref)
18410 .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
18411 })?;
18412
18413 Ok((sheet, row, col))
18414 }
18415
18416 fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
18418 use formualizer_parse::parser::ASTNodeType;
18419 match &ast.node_type {
18420 ASTNodeType::Function { name, args, .. } => {
18421 if let Some(func) = self
18422 .get_function("", name)
18423 .or_else(|| crate::function_registry::get("", name))
18424 && crate::external_callbacks::metadata(
18425 func.as_ref(),
18426 crate::function::FnCaps::empty,
18427 || func.caps(),
18428 )
18429 .contains(crate::function::FnCaps::VOLATILE)
18430 {
18431 return true;
18432 }
18433 args.iter()
18434 .any(|arg| self.is_ast_volatile_with_provider(arg))
18435 }
18436 ASTNodeType::BinaryOp { left, right, .. } => {
18437 self.is_ast_volatile_with_provider(left)
18438 || self.is_ast_volatile_with_provider(right)
18439 }
18440 ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
18441 ASTNodeType::Array(rows) => rows.iter().any(|row| {
18442 row.iter()
18443 .any(|cell| self.is_ast_volatile_with_provider(cell))
18444 }),
18445 _ => false,
18446 }
18447 }
18448
18449 fn evaluate_layer_sequential(
18451 &mut self,
18452 layer: &super::scheduler::Layer,
18453 ) -> Result<usize, ExcelError> {
18454 self.resource_checkpoint(layer.vertices.len() as u64)?;
18455 self.evaluate_layer_sequential_effects(layer)
18456 }
18457
18458 fn update_vertex_value_with_delta(
18459 &mut self,
18460 vertex_id: VertexId,
18461 new_value: LiteralValue,
18462 delta: &mut DeltaCollector,
18463 ) {
18464 if delta.mode != DeltaMode::Off
18465 && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
18466 {
18467 let sheet_name = self.graph.sheet_name(cell.sheet_id);
18468 let old = self
18469 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18470 .unwrap_or(LiteralValue::Empty);
18471 if old != new_value {
18472 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18473 }
18474 }
18475 self.graph.update_vertex_value_ref(vertex_id, &new_value);
18476 self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
18477 }
18478
18479 fn evaluate_layer_sequential_with_delta(
18480 &mut self,
18481 layer: &super::scheduler::Layer,
18482 delta: &mut DeltaCollector,
18483 ) -> Result<usize, ExcelError> {
18484 self.resource_checkpoint(layer.vertices.len() as u64)?;
18485 self.evaluate_layer_sequential_with_delta_effects(layer, delta)
18486 }
18487
18488 fn evaluate_layer_sequential_cancellable(
18490 &mut self,
18491 layer: &super::scheduler::Layer,
18492 cancel_flag: &AtomicBool,
18493 ) -> Result<usize, ExcelError> {
18494 self.resource_checkpoint(layer.vertices.len() as u64)?;
18495 self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
18496 }
18497
18498 fn evaluate_layer_sequential_cancellable_demand_driven(
18500 &mut self,
18501 layer: &super::scheduler::Layer,
18502 cancel_flag: &AtomicBool,
18503 ) -> Result<usize, ExcelError> {
18504 self.resource_checkpoint(layer.vertices.len() as u64)?;
18505 self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
18506 }
18507
18508 fn evaluate_layer_parallel(
18520 &mut self,
18521 layer: &super::scheduler::Layer,
18522 ) -> Result<usize, ExcelError> {
18523 if layer.sequential {
18524 return self.evaluate_layer_sequential(layer);
18525 }
18526 self.resource_checkpoint(layer.vertices.len() as u64)?;
18527 let len = layer.vertices.len();
18528 let buffered = buffer_layer_writes(layer);
18529 let (probe, worth) = (PARALLEL_LAYER_PROBE, PARALLEL_LAYER_WORTH);
18530 let start = crate::instant::FzInstant::now();
18531 let mut pos = 0usize;
18532 let mut step = 1usize;
18533 while pos < len {
18534 if pos > 0 {
18535 let elapsed = start.elapsed();
18536 let per_vertex = elapsed.as_nanos() / pos as u128 + 1;
18538 let rest_estimate = per_vertex * (len - pos) as u128;
18539 if len - pos >= 2 && (elapsed >= probe || rest_estimate >= worth.as_nanos()) {
18540 let rest = layer.sub_layer(pos, len);
18541 let min_chunk = if per_vertex >= EXPENSIVE_VERTEX_NS {
18544 1
18545 } else {
18546 8
18547 };
18548 return Ok(pos + self.evaluate_layer_parallel_effects(&rest, min_chunk)?);
18549 }
18550 let fit = (probe.saturating_sub(elapsed).as_nanos() / per_vertex) as usize + 1;
18553 step = step.saturating_mul(2).min(fit);
18554 }
18555 let end = (pos + step).min(len);
18556 let slice = layer.sub_layer(pos, end);
18557 pos += self.evaluate_layer_units_until(
18560 &slice,
18561 None,
18562 None,
18563 None,
18564 buffered,
18565 Some(start + probe),
18566 )?;
18567 }
18568 Ok(len)
18569 }
18570
18571 fn evaluate_layer_parallel_with_delta(
18572 &mut self,
18573 layer: &super::scheduler::Layer,
18574 delta: &mut DeltaCollector,
18575 ) -> Result<usize, ExcelError> {
18576 if layer.sequential {
18577 return self.evaluate_layer_sequential_with_delta(layer, delta);
18578 }
18579 self.resource_checkpoint(layer.vertices.len() as u64)?;
18580 self.evaluate_layer_parallel_with_delta_effects(layer, delta)
18581 }
18582
18583 fn evaluate_layer_parallel_cancellable(
18585 &mut self,
18586 layer: &super::scheduler::Layer,
18587 cancel_flag: &AtomicBool,
18588 ) -> Result<usize, ExcelError> {
18589 if layer.sequential {
18590 return self.evaluate_layer_sequential_cancellable(layer, cancel_flag);
18591 }
18592 self.resource_checkpoint(layer.vertices.len() as u64)?;
18593 self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
18594 }
18595
18596 fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
18598 let _callbacks = crate::external_callbacks::enter_if_missing(&self.resolver.callbacks);
18599 if !self.graph.vertex_exists(vertex_id) {
18601 return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
18602 .with_message(format!("Vertex not found: {vertex_id:?}")));
18603 }
18604
18605 let kind = self.graph.get_vertex_kind(vertex_id);
18607 let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
18608
18609 let view = match kind {
18610 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18611 if let Some(view) = self.graph.formula_view(vertex_id) {
18612 view
18613 } else {
18614 return Ok(LiteralValue::Number(0.0));
18615 }
18616 }
18617 VertexKind::Empty | VertexKind::Cell => {
18618 if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
18619 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18620 let row = cell_ref.coord.row() + 1;
18621 let col = cell_ref.coord.col() + 1;
18622 if let Some(v) = self.read_cell_value(sheet_name, row, col) {
18623 return Ok(v);
18624 }
18625 }
18626 return Ok(LiteralValue::Number(0.0));
18627 }
18628 VertexKind::NamedScalar => {
18629 let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
18630 ExcelError::new(ExcelErrorKind::Name)
18631 .with_message("Named range metadata missing".to_string())
18632 })?;
18633
18634 return match &named_range.definition {
18635 NamedDefinition::Cell(cell_ref) => {
18636 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18637 Ok(self
18638 .read_cell_value(
18639 sheet_name,
18640 cell_ref.coord.row() + 1,
18641 cell_ref.coord.col() + 1,
18642 )
18643 .unwrap_or(LiteralValue::Empty))
18644 }
18645 NamedDefinition::Literal(v) => Ok(v.clone()),
18646 NamedDefinition::Formula { ast, .. } => {
18647 let _origin = self
18648 .operation_diagnostic_collector
18649 .as_ref()
18650 .map(|c| c.named_scope(vertex_id));
18651 let context_sheet = match named_range.scope {
18652 NameScope::Sheet(id) => id,
18653 NameScope::Workbook => sheet_id,
18654 };
18655 let sheet_name = self.graph.sheet_name(context_sheet);
18656 let cell_ref = self
18657 .graph
18658 .get_cell_ref(vertex_id)
18659 .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
18660 if let Some(result) = self
18661 .freshness_evaluate_name_recorded(vertex_id, sheet_name, cell_ref, ast)
18662 {
18663 return result;
18664 }
18665 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18666 .with_resolved_functions(&self.resolved_functions)
18667 .with_arena_cells(self);
18668 interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
18669 }
18670 NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
18671 .with_message("Range-valued name evaluated as scalar".to_string())),
18672 };
18673 }
18674 VertexKind::NamedArray => {
18675 let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
18676 ExcelError::new(ExcelErrorKind::Name)
18677 .with_message("Named range metadata missing".to_string())
18678 })?;
18679
18680 return match &named_range.definition {
18681 NamedDefinition::Range(range_ref) => {
18682 if range_ref.start.sheet_id != range_ref.end.sheet_id {
18683 return Err(ExcelError::new(ExcelErrorKind::Ref)
18684 .with_message("Named range cannot span sheets".to_string()));
18685 }
18686 let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
18687 let sr0 = range_ref.start.coord.row();
18688 let sc0 = range_ref.start.coord.col();
18689 let er0 = range_ref.end.coord.row();
18690 let ec0 = range_ref.end.coord.col();
18691 if sr0 > er0 || sc0 > ec0 {
18692 return Err(ExcelError::new(ExcelErrorKind::Ref)
18693 .with_message("Invalid named range bounds".to_string()));
18694 }
18695
18696 let h = (er0 - sr0 + 1) as usize;
18697 let w = (ec0 - sc0 + 1) as usize;
18698 let cell_count = (h as u64).saturating_mul(w as u64);
18699 if cell_count > self.config.spill.max_spill_cells as u64 {
18700 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18701 "Named range too large to materialize as an array".to_string(),
18702 ));
18703 }
18704
18705 let sheet_id = range_ref.start.sheet_id;
18707 let asheet = self.arrow_sheets.sheet(sheet_name);
18708 let mut rows = Vec::with_capacity(h);
18709 for r0 in sr0..=er0 {
18710 let mut row = Vec::with_capacity(w);
18711 for c0 in sc0..=ec0 {
18712 row.push(
18713 self.read_cell_formatted_in(sheet_id, asheet, r0 + 1, c0 + 1)
18714 .0,
18715 );
18716 }
18717 rows.push(row);
18718 }
18719 Ok(LiteralValue::Array(rows))
18720 }
18721 NamedDefinition::Cell(cell_ref) => {
18722 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18723 let row = cell_ref.coord.row() + 1;
18724 let col = cell_ref.coord.col() + 1;
18725 let v = self
18726 .read_cell_value(sheet_name, row, col)
18727 .unwrap_or(LiteralValue::Empty);
18728 Ok(LiteralValue::Array(vec![vec![v]]))
18729 }
18730 NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
18731 NamedDefinition::Formula { ast, .. } => {
18732 let _origin = self
18733 .operation_diagnostic_collector
18734 .as_ref()
18735 .map(|c| c.named_scope(vertex_id));
18736 let context_sheet = match named_range.scope {
18737 NameScope::Sheet(id) => id,
18738 NameScope::Workbook => sheet_id,
18739 };
18740 let sheet_name = self.graph.sheet_name(context_sheet);
18741 let cell_ref = self
18742 .graph
18743 .get_cell_ref(vertex_id)
18744 .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
18745 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18746 .with_resolved_functions(&self.resolved_functions)
18747 .with_arena_cells(self);
18748 let result = self
18749 .freshness_evaluate_name_recorded(vertex_id, sheet_name, cell_ref, ast)
18750 .unwrap_or_else(|| {
18751 interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
18752 });
18753 match result {
18754 Ok(v) => match v {
18755 LiteralValue::Array(_) => Ok(v),
18756 other => Ok(LiteralValue::Array(vec![vec![other]])),
18757 },
18758 Err(err) => Ok(LiteralValue::Error(err)),
18759 }
18760 }
18761 };
18762 }
18763 VertexKind::InfiniteRange
18764 | VertexKind::Range
18765 | VertexKind::External
18766 | VertexKind::Table => {
18767 return Ok(LiteralValue::Number(0.0));
18769 }
18770 };
18771
18772 let sheet_name = self.graph.sheet_name(sheet_id);
18774 let cell_ref = self
18775 .graph
18776 .get_cell_ref(vertex_id)
18777 .expect("cell ref for vertex");
18778 if let Some(result) =
18779 self.freshness_evaluate_recorded(vertex_id, sheet_name, cell_ref, view)
18780 {
18781 return result;
18782 }
18783 let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18784 .with_resolved_functions(&self.resolved_functions)
18785 .with_arena_cells(self);
18786
18787 interpreter
18788 .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
18789 .map(|cv| {
18790 let format = cv.format_id();
18791 self.record_derived_format(vertex_id, format);
18792 self.materialize_formula_result(vertex_id, cv)
18793 })
18794 .or_else(|error| self.fit_formula_error(vertex_id, error))
18795 }
18796
18797 pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
18799 self.thread_pool.as_ref()
18800 }
18801}
18802
18803#[derive(Default)]
18804struct RowBoundsCache {
18805 snapshot: u64,
18806 map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
18808}
18809
18810impl RowBoundsCache {
18811 fn new(snapshot: u64) -> Self {
18812 Self {
18813 snapshot,
18814 map: Default::default(),
18815 }
18816 }
18817 fn get_row_bounds(
18818 &self,
18819 sheet_id: SheetId,
18820 col_idx: usize,
18821 snapshot: u64,
18822 ) -> Option<(Option<u32>, Option<u32>)> {
18823 if self.snapshot != snapshot {
18824 return None;
18825 }
18826 self.map.get(&(sheet_id as u32, col_idx)).copied()
18827 }
18828 fn put_row_bounds(
18829 &mut self,
18830 sheet_id: SheetId,
18831 col_idx: usize,
18832 snapshot: u64,
18833 bounds: (Option<u32>, Option<u32>),
18834 ) {
18835 if self.snapshot != snapshot {
18836 self.snapshot = snapshot;
18837 self.map.clear();
18838 }
18839 self.map.insert((sheet_id as u32, col_idx), bounds);
18840 }
18841}
18842
18843struct UsedAxisBoundsCache {
18844 snapshot: u64,
18845 row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
18846 col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
18847 #[cfg(test)]
18848 row_hits: std::sync::atomic::AtomicUsize,
18849 #[cfg(test)]
18850 row_misses: std::sync::atomic::AtomicUsize,
18851 #[cfg(test)]
18852 col_hits: std::sync::atomic::AtomicUsize,
18853 #[cfg(test)]
18854 col_misses: std::sync::atomic::AtomicUsize,
18855}
18856
18857impl UsedAxisBoundsCache {
18858 fn new(snapshot: u64) -> Self {
18859 Self {
18860 snapshot,
18861 row_bounds_by_col_span: Default::default(),
18862 col_bounds_by_row_span: Default::default(),
18863 #[cfg(test)]
18864 row_hits: std::sync::atomic::AtomicUsize::new(0),
18865 #[cfg(test)]
18866 row_misses: std::sync::atomic::AtomicUsize::new(0),
18867 #[cfg(test)]
18868 col_hits: std::sync::atomic::AtomicUsize::new(0),
18869 #[cfg(test)]
18870 col_misses: std::sync::atomic::AtomicUsize::new(0),
18871 }
18872 }
18873
18874 fn reset_for_snapshot(&mut self, snapshot: u64) {
18875 if self.snapshot != snapshot {
18876 self.snapshot = snapshot;
18877 self.row_bounds_by_col_span.clear();
18878 self.col_bounds_by_row_span.clear();
18879 }
18880 }
18881
18882 fn get_row_bounds(
18883 &self,
18884 sheet_id: SheetId,
18885 start_col: u32,
18886 end_col: u32,
18887 snapshot: u64,
18888 ) -> Option<Option<(u32, u32)>> {
18889 if self.snapshot != snapshot {
18890 return None;
18891 }
18892 let cached = self
18893 .row_bounds_by_col_span
18894 .get(&(sheet_id, start_col, end_col))
18895 .copied();
18896 #[cfg(test)]
18897 if cached.is_some() {
18898 self.row_hits.fetch_add(1, Ordering::Relaxed);
18899 }
18900 cached
18901 }
18902
18903 fn put_row_bounds(
18904 &mut self,
18905 sheet_id: SheetId,
18906 start_col: u32,
18907 end_col: u32,
18908 snapshot: u64,
18909 bounds: Option<(u32, u32)>,
18910 ) {
18911 self.reset_for_snapshot(snapshot);
18912 self.row_bounds_by_col_span
18913 .insert((sheet_id, start_col, end_col), bounds);
18914 #[cfg(test)]
18915 self.row_misses.fetch_add(1, Ordering::Relaxed);
18916 }
18917
18918 fn get_col_bounds(
18919 &self,
18920 sheet_id: SheetId,
18921 start_row: u32,
18922 end_row: u32,
18923 snapshot: u64,
18924 ) -> Option<Option<(u32, u32)>> {
18925 if self.snapshot != snapshot {
18926 return None;
18927 }
18928 let cached = self
18929 .col_bounds_by_row_span
18930 .get(&(sheet_id, start_row, end_row))
18931 .copied();
18932 #[cfg(test)]
18933 if cached.is_some() {
18934 self.col_hits.fetch_add(1, Ordering::Relaxed);
18935 }
18936 cached
18937 }
18938
18939 fn put_col_bounds(
18940 &mut self,
18941 sheet_id: SheetId,
18942 start_row: u32,
18943 end_row: u32,
18944 snapshot: u64,
18945 bounds: Option<(u32, u32)>,
18946 ) {
18947 self.reset_for_snapshot(snapshot);
18948 self.col_bounds_by_row_span
18949 .insert((sheet_id, start_row, end_row), bounds);
18950 #[cfg(test)]
18951 self.col_misses.fetch_add(1, Ordering::Relaxed);
18952 }
18953}
18954
18955#[derive(Default)]
18957pub struct ShimSpillManager {
18958 region_locks: RegionLockManager,
18959 pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
18960}
18961
18962impl ShimSpillManager {
18963 pub(crate) fn reserve(
18964 &mut self,
18965 owner: VertexId,
18966 anchor_cell: CellRef,
18967 shape: SpillShape,
18968 _meta: SpillMeta,
18969 ) -> Result<(), ExcelError> {
18970 let region = crate::engine::spill::Region {
18972 sheet_id: anchor_cell.sheet_id as u32,
18973 row_start: anchor_cell.coord.row(),
18974 row_end: anchor_cell
18975 .coord
18976 .row()
18977 .saturating_add(shape.rows)
18978 .saturating_sub(1),
18979 col_start: anchor_cell.coord.col(),
18980 col_end: anchor_cell
18981 .coord
18982 .col()
18983 .saturating_add(shape.cols)
18984 .saturating_sub(1),
18985 };
18986 match self.region_locks.reserve(region, owner) {
18987 Ok(id) => {
18988 if id != 0 {
18989 self.active_locks.insert(owner, id);
18990 }
18991 Ok(())
18992 }
18993 Err(e) => Err(e),
18994 }
18995 }
18996
18997 pub(crate) fn release_owner(&mut self, owner: VertexId) {
19003 if let Some(id) = self.active_locks.remove(&owner) {
19004 self.region_locks.release(id);
19005 }
19006 }
19007
19008 pub(crate) fn commit_array_with_value_probe<F>(
19009 &mut self,
19010 graph: &mut DependencyGraph,
19011 anchor_vertex: VertexId,
19012 targets: &[CellRef],
19013 rows: Vec<Vec<LiteralValue>>,
19014 mut value_probe: F,
19015 ) -> Result<(), ExcelError>
19016 where
19017 F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
19018 {
19019 use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
19020
19021 let plan_res = graph.plan_spill_region(anchor_vertex, targets);
19025 if let Err(e) = plan_res {
19026 if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19027 self.region_locks.release(id);
19028 }
19029 return Err(e);
19030 }
19031
19032 if !graph.value_cache_enabled() {
19033 let (expected_rows, expected_cols) = if targets.is_empty() {
19035 (0u32, 0u32)
19036 } else {
19037 let mut min_r = u32::MAX;
19038 let mut max_r = 0u32;
19039 let mut min_c = u32::MAX;
19040 let mut max_c = 0u32;
19041 for cell in targets {
19042 let r = cell.coord.row();
19043 let c = cell.coord.col();
19044 min_r = min_r.min(r);
19045 max_r = max_r.max(r);
19046 min_c = min_c.min(c);
19047 max_c = max_c.max(c);
19048 }
19049 (
19050 max_r.saturating_sub(min_r).saturating_add(1),
19051 max_c.saturating_sub(min_c).saturating_add(1),
19052 )
19053 };
19054
19055 let anchor_cell = graph
19056 .get_cell_ref(anchor_vertex)
19057 .expect("anchor cell ref for spill commit");
19058
19059 for cell in targets {
19060 if *cell == anchor_cell {
19062 continue;
19063 }
19064 if graph.spill_registry_anchor_for_cell(*cell).is_some() {
19066 continue;
19067 }
19068 if let Some(vid) = graph.get_vertex_id_for_address(cell)
19070 && vid != anchor_vertex
19071 {
19072 match graph.get_vertex_kind(vid) {
19073 crate::engine::vertex::VertexKind::FormulaScalar
19074 | crate::engine::vertex::VertexKind::FormulaArray => {
19075 continue;
19077 }
19078 _ => {}
19079 }
19080 }
19081
19082 if let Some(v) = value_probe(graph, cell)
19083 && !matches!(v, LiteralValue::Empty)
19084 {
19085 if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19086 self.region_locks.release(id);
19087 }
19088 return Err(ExcelError::new(ExcelErrorKind::Spill)
19089 .with_message("BlockedByValue")
19090 .with_extra(ExcelErrorExtra::Spill {
19091 expected_rows,
19092 expected_cols,
19093 }));
19094 }
19095 }
19096 }
19097
19098 let commit_res = graph.commit_spill_region_atomic_with_fault(
19099 anchor_vertex,
19100 targets.to_vec(),
19101 rows,
19102 None,
19103 );
19104 if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19105 self.region_locks.release(id);
19106 }
19107 commit_res.map(|_| ())
19108 }
19109
19110 pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
19112 &mut self,
19113 engine: &mut Engine<R>,
19114 anchor_vertex: VertexId,
19115 targets: &[CellRef],
19116 rows: Vec<Vec<LiteralValue>>,
19117 ) -> Result<(), ExcelError> {
19118 if let Err(error) = engine.guard_pending_spill_commit(anchor_vertex, targets) {
19119 self.release_owner(anchor_vertex);
19120 return Err(error);
19121 }
19122 let plan_res = engine.graph.plan_spill_region(anchor_vertex, targets);
19124 if let Err(e) = plan_res {
19125 if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19126 self.region_locks.release(id);
19127 }
19128 return Err(e);
19129 }
19130
19131 let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
19132 anchor_vertex,
19133 targets.to_vec(),
19134 rows.clone(),
19135 None,
19136 );
19137 if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19138 self.region_locks.release(id);
19139 }
19140 commit_res.map(|_| ())?;
19141 engine
19142 .blocked_pending_spills
19143 .retain(|entry| entry.0 != anchor_vertex);
19144 engine.spill_blocker_waiters.forget(anchor_vertex);
19145
19146 if engine.config.arrow_storage_enabled
19148 && engine.config.delta_overlay_enabled
19149 && engine.config.write_formula_overlay_enabled
19150 {
19151 for (idx, cell) in targets.iter().enumerate() {
19153 let (r_off, c_off) = {
19154 if rows.is_empty() || rows[0].is_empty() {
19155 (0usize, 0usize)
19156 } else {
19157 let width = rows[0].len();
19158 (idx / width, idx % width)
19159 }
19160 };
19161 let v = rows
19162 .get(r_off)
19163 .and_then(|r| r.get(c_off))
19164 .cloned()
19165 .unwrap_or(LiteralValue::Empty);
19166 let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
19167 engine.mirror_value_to_computed_overlay(
19168 &sheet_name,
19169 cell.coord.row() + 1,
19170 cell.coord.col() + 1,
19171 &v,
19172 );
19173 }
19174 }
19175 Ok(())
19176 }
19177}
19178
19179impl<R> Engine<R>
19180where
19181 R: EvaluationContext,
19182{
19183 fn resolve_shared_ref(
19184 &self,
19185 reference: &ReferenceType,
19186 current_sheet: &str,
19187 ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
19188 use formualizer_common::{
19189 SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
19190 SheetRef as SharedRef,
19191 };
19192
19193 let sr = match reference {
19195 ReferenceType::Cell {
19196 sheet,
19197 row,
19198 col,
19199 row_abs,
19200 col_abs,
19201 } => {
19202 let row0 = row
19203 .checked_sub(1)
19204 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19205 let col0 = col
19206 .checked_sub(1)
19207 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19208 let sheet_loc = match sheet.as_deref() {
19209 Some(name) => SheetLocator::from_name(name),
19210 None => SheetLocator::Current,
19211 };
19212 let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
19213 SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
19214 }
19215 ReferenceType::Range {
19216 sheet,
19217 start_row,
19218 start_col,
19219 end_row,
19220 end_col,
19221 start_row_abs,
19222 start_col_abs,
19223 end_row_abs,
19224 end_col_abs,
19225 } => {
19226 let sheet_loc = match sheet.as_deref() {
19227 Some(name) => SheetLocator::from_name(name),
19228 None => SheetLocator::Current,
19229 };
19230 let sr = start_row
19231 .map(|r| {
19232 r.checked_sub(1)
19233 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19234 })
19235 .transpose()?;
19236 let sc = start_col
19237 .map(|c| {
19238 c.checked_sub(1)
19239 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19240 })
19241 .transpose()?;
19242 let er = end_row
19243 .map(|r| {
19244 r.checked_sub(1)
19245 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19246 })
19247 .transpose()?;
19248 let ec = end_col
19249 .map(|c| {
19250 c.checked_sub(1)
19251 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19252 })
19253 .transpose()?;
19254 let range = SharedRangeRef::from_parts(
19255 sheet_loc,
19256 sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
19257 sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
19258 er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
19259 ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
19260 )
19261 .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
19262 SharedRef::Range(range)
19263 }
19264 _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
19265 };
19266
19267 let current_id = self
19268 .graph
19269 .sheet_id(current_sheet)
19270 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19271
19272 let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
19273 match loc {
19274 SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
19275 SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
19276 SheetLocator::Name(name) => {
19277 let n = name.as_ref();
19278 self.graph
19279 .sheet_id(n)
19280 .map(SheetLocator::Id)
19281 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19282 }
19283 }
19284 };
19285
19286 match sr {
19287 SharedRef::Cell(cell) => {
19288 let owned = cell.into_owned();
19289 let sheet = resolve_loc(owned.sheet)?;
19290 Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
19291 }
19292 SharedRef::Range(range) => {
19293 let owned = range.into_owned();
19294 let sheet = resolve_loc(owned.sheet)?;
19295 Ok(SharedRef::Range(SharedRangeRef {
19296 sheet,
19297 start_row: owned.start_row,
19298 start_col: owned.start_col,
19299 end_row: owned.end_row,
19300 end_col: owned.end_col,
19301 }))
19302 }
19303 }
19304 }
19305}
19306
19307impl<R> crate::traits::ReferenceResolver for Engine<R>
19310where
19311 R: EvaluationContext,
19312{
19313 fn resolve_cell_reference(
19314 &self,
19315 sheet: Option<&str>,
19316 row: u32,
19317 col: u32,
19318 ) -> Result<LiteralValue, ExcelError> {
19319 let Some(sheet_name) = sheet else {
19330 return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
19331 "Unqualified cell reference resolved without sheet context".to_string(),
19332 ));
19333 };
19334 if let Some(v) = self.read_cell_value(sheet_name, row, col) {
19337 Ok(v)
19338 } else {
19339 Ok(LiteralValue::Number(0.0))
19341 }
19342 }
19343}
19344
19345impl<R> crate::traits::RangeResolver for Engine<R>
19346where
19347 R: EvaluationContext,
19348{
19349 fn resolve_range_reference(
19350 &self,
19351 sheet: Option<&str>,
19352 sr: Option<u32>,
19353 sc: Option<u32>,
19354 er: Option<u32>,
19355 ec: Option<u32>,
19356 ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
19357 self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
19360 }
19361}
19362
19363impl<R> crate::traits::NamedRangeResolver for Engine<R>
19364where
19365 R: EvaluationContext,
19366{
19367 fn resolve_named_range_reference(
19368 &self,
19369 name: &str,
19370 ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
19371 self.resolver.resolve_named_range_reference(name)
19372 }
19373}
19374
19375impl<R> crate::traits::TableResolver for Engine<R>
19376where
19377 R: EvaluationContext,
19378{
19379 fn resolve_table_reference(
19380 &self,
19381 tref: &formualizer_parse::parser::TableReference,
19382 ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
19383 self.resolver.resolve_table_reference(tref)
19384 }
19385}
19386
19387impl<R> crate::traits::SourceResolver for Engine<R>
19388where
19389 R: EvaluationContext,
19390{
19391 fn source_scalar_version(&self, name: &str) -> Option<u64> {
19392 self.resolver.source_scalar_version(name)
19393 }
19394
19395 fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
19396 self.resolver.resolve_source_scalar(name)
19397 }
19398
19399 fn source_table_version(&self, name: &str) -> Option<u64> {
19400 self.resolver.source_table_version(name)
19401 }
19402
19403 fn resolve_source_table(
19404 &self,
19405 name: &str,
19406 ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
19407 self.resolver.resolve_source_table(name)
19408 }
19409}
19410
19411impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
19413
19414impl<R: EvaluationContext> crate::interpreter::ArenaCellResolver for Engine<R> {
19416 fn resolve_arena_range(
19417 &self,
19418 reference: &crate::engine::arena::CompactRefType,
19419 current: SheetId,
19420 row_delta: i64,
19421 col_delta: i64,
19422 ) -> Result<RangeView<'_>, ExcelError> {
19423 use crate::engine::arena::{CompactRefType, SheetKey};
19424 let CompactRefType::Range {
19425 sheet,
19426 start_row,
19427 start_col,
19428 end_row,
19429 end_col,
19430 start_row_abs,
19431 start_col_abs,
19432 end_row_abs,
19433 end_col_abs,
19434 } = reference
19435 else {
19436 return Err(ExcelError::new(ExcelErrorKind::Ref));
19437 };
19438 let sheet_id = match sheet {
19439 None => current,
19440 Some(SheetKey::Id(id)) => *id,
19441 Some(SheetKey::Name(id)) => self
19442 .graph
19443 .sheet_id(self.graph.data_store().resolve_ast_string(*id))
19444 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?,
19445 };
19446 if self.graph.sheet_reg().name(sheet_id).is_empty() {
19447 return Err(ExcelError::new(ExcelErrorKind::Ref));
19448 }
19449 let axis = |value, sentinel, delta, absolute| {
19450 if value == sentinel {
19451 Ok(None)
19452 } else {
19453 crate::interpreter::shift_axis_for_offset(value, delta, absolute).map(Some)
19454 }
19455 };
19456 let sr = axis(*start_row, 0, row_delta, *start_row_abs)?;
19457 let sc = axis(*start_col, 0, col_delta, *start_col_abs)?;
19458 let er = axis(*end_row, u32::MAX, row_delta, *end_row_abs)?;
19459 let ec = axis(*end_col, u32::MAX, col_delta, *end_col_abs)?;
19460 if sr.zip(er).is_some_and(|(s, e)| s > e) || sc.zip(ec).is_some_and(|(s, e)| s > e) {
19461 return Err(ExcelError::new(ExcelErrorKind::Ref));
19462 }
19463 self.resolve_range_coordinates(sheet_id, sr, sc, er, ec)
19464 }
19465
19466 fn resolve_arena_cell(
19467 &self,
19468 sheet: Option<crate::engine::arena::SheetKey>,
19469 current: crate::SheetId,
19470 row: u32,
19471 col: u32,
19472 ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
19473 use crate::engine::arena::SheetKey;
19474 let (sheet_id, named_arrow) = match sheet {
19475 None => (current, None),
19476 Some(SheetKey::Id(id)) => {
19477 if self.graph.sheet_reg().name(id).is_empty() {
19478 return Err(ExcelError::new(ExcelErrorKind::Ref));
19479 }
19480 (id, None)
19481 }
19482 Some(SheetKey::Name(id)) => {
19483 let (sheet_id, arrow) = self
19484 .resolved_sheet_names
19485 .get(&id)
19486 .copied()
19487 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19488 (sheet_id, Some(arrow))
19489 }
19490 };
19491 let arrow = named_arrow.unwrap_or_else(|| {
19492 self.resolved_arrow_sheets
19493 .get(usize::from(sheet_id))
19494 .copied()
19495 .flatten()
19496 });
19497 let asheet = arrow
19498 .and_then(|index| self.arrow_sheets.sheets.get(index))
19499 .or_else(|| {
19500 let name = match sheet {
19504 Some(SheetKey::Name(id)) => self.graph.data_store().resolve_ast_string(id),
19505 _ => self.graph.sheet_name(sheet_id),
19506 };
19507 self.arrow_sheets.sheet(name)
19508 });
19509 Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
19510 }
19511}
19512
19513impl<R> crate::traits::FunctionProvider for Engine<R>
19514where
19515 R: EvaluationContext,
19516{
19517 fn planning_semantic_revision(&self) -> Option<u64> {
19518 self.resolver.planning_semantic_revision()
19519 }
19520
19521 fn get_function(
19522 &self,
19523 prefix: &str,
19524 name: &str,
19525 ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
19526 self.resolver.get_function(prefix, name)
19527 }
19528
19529 fn get_function_for_planning(
19530 &self,
19531 prefix: &str,
19532 name: &str,
19533 ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
19534 self.resolver.get_function_for_planning(prefix, name)
19535 }
19536}
19537
19538impl<R> Engine<R>
19539where
19540 R: EvaluationContext,
19541{
19542 fn resolve_range_coordinates(
19543 &self,
19544 sheet_id: SheetId,
19545 sr: Option<u32>,
19546 sc: Option<u32>,
19547 er: Option<u32>,
19548 ec: Option<u32>,
19549 ) -> Result<RangeView<'_>, ExcelError> {
19550 let sheet_name = self.graph.sheet_name(sheet_id);
19551 let extent = resolve_used_extent_with_fallback(
19552 OpenRangeBounds {
19553 start_row: sr,
19554 start_column: sc,
19555 end_row: er,
19556 end_column: ec,
19557 },
19558 ExtentPolicy::EvaluationCompat {
19559 fallback_row: None,
19560 fallback_column: None,
19561 },
19562 || {
19563 self.sheet_bounds(sheet_name)
19564 .map(|_| self.config.max_open_ended_rows)
19565 },
19566 || {
19567 self.sheet_bounds(sheet_name)
19568 .map(|_| self.config.max_open_ended_cols)
19569 },
19570 |first, last| self.used_rows_for_columns(sheet_name, first, last),
19571 |first, last| self.used_cols_for_rows(sheet_name, first, last),
19572 );
19573 let (sr, sc, er, ec) = extent
19574 .map(|e| (e.start_row, e.start_column, e.end_row, e.end_column))
19575 .unwrap_or((1, 1, 0, 0));
19576 if self.force_materialize_range_views {
19577 if er < sr || ec < sc {
19578 return Ok(RangeView::from_owned_rows(
19579 Vec::new(),
19580 self.config.date_system,
19581 ));
19582 }
19583 let h = (er - sr + 1) as u64;
19584 let w = (ec - sc + 1) as u64;
19585 if h.saturating_mul(w) <= self.config.spill.max_spill_cells as u64 {
19586 let mut rows = Vec::with_capacity(h as usize);
19587 for r in sr..=er {
19588 let mut row = Vec::with_capacity(w as usize);
19589 for c in sc..=ec {
19590 row.push(
19591 self.read_cell_value(sheet_name, r, c)
19592 .unwrap_or(LiteralValue::Empty),
19593 );
19594 }
19595 rows.push(row);
19596 }
19597 return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
19598 }
19599 }
19600 let Some(asheet) = self.arrow_sheet_by_id(sheet_id) else {
19601 return Ok(RangeView::from_owned_rows(
19602 Vec::new(),
19603 self.config.date_system,
19604 ));
19605 };
19606 Ok(if er < sr || ec < sc {
19607 asheet.range_view(1, 1, 0, 0)
19608 } else {
19609 asheet.range_view(
19610 sr.saturating_sub(1) as usize,
19611 sc.saturating_sub(1) as usize,
19612 er.saturating_sub(1) as usize,
19613 ec.saturating_sub(1) as usize,
19614 )
19615 })
19616 }
19617
19618 pub(crate) fn semantic_used_rows_for_columns(
19626 &self,
19627 sheet: &str,
19628 start_col: u32,
19629 end_col: u32,
19630 ) -> Option<(u32, u32)> {
19631 let arrow_bounds = self
19632 .sheet_store()
19633 .sheet(sheet)
19634 .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
19635 let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
19636 Self::union_used_bounds(arrow_bounds, formula_bounds)
19637 }
19638
19639 pub(crate) fn semantic_used_cols_for_rows(
19640 &self,
19641 sheet: &str,
19642 start_row: u32,
19643 end_row: u32,
19644 ) -> Option<(u32, u32)> {
19645 let arrow_bounds = self
19646 .sheet_store()
19647 .sheet(sheet)
19648 .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
19649 let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
19650 Self::union_used_bounds(arrow_bounds, formula_bounds)
19651 }
19652}
19653
19654impl<R> crate::traits::EvaluationContext for Engine<R>
19656where
19657 R: EvaluationContext,
19658{
19659 fn named_binding_presence(&self, name: &str, current_sheet: &str) -> Option<bool> {
19660 let sheet = self.graph.sheet_id(current_sheet)?;
19661 if self.graph.resolve_name_entry(name, sheet).is_some() {
19662 Some(true)
19663 } else {
19664 self.resolver.named_binding_presence(name, current_sheet)
19665 }
19666 }
19667 fn operation_diagnostic_collector(
19668 &self,
19669 ) -> Option<&crate::operation_diagnostics::OperationDiagnosticCollector> {
19670 self.operation_diagnostic_collector.as_deref()
19671 }
19672
19673 fn clock(&self) -> &dyn crate::timezone::ClockProvider {
19674 &self.clock
19675 }
19676
19677 fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
19678 self.thread_pool.as_ref()
19679 }
19680
19681 fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
19682 self.active_cancel_flag.clone()
19683 }
19684
19685 fn chunk_hint(&self) -> Option<usize> {
19686 let hint =
19688 (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
19689 Some(hint.clamp(1024, 1 << 20)) }
19691
19692 fn materialized_array_cells(&self) -> u64 {
19693 self.config
19694 .evaluation_budgets
19695 .scratch
19696 .materialized_array_cells
19697 .unwrap_or(crate::engine::resource_ledger::DEFAULT_MATERIALIZED_ARRAY_CELLS)
19698 }
19699
19700 fn volatile_level(&self) -> crate::traits::VolatileLevel {
19701 self.config.volatile_level
19702 }
19703
19704 fn workbook_seed(&self) -> u64 {
19705 self.config.workbook_seed
19706 }
19707
19708 fn recalc_epoch(&self) -> u64 {
19709 self.recalc_epoch
19710 }
19711
19712 fn workbook_sheet_count(&self) -> Option<usize> {
19713 Some(self.graph.sheet_reg().active_len())
19714 }
19715
19716 fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
19717 self.graph.sheet_reg().active_position(sheet)
19718 }
19719
19720 fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
19721 self.sheet_index_by_name(current_sheet)
19722 }
19723
19724 fn inspect_reference(
19725 &self,
19726 reference: &ReferenceType,
19727 current_sheet: &str,
19728 ) -> Result<Option<ReferenceInfo>, ExcelError> {
19729 let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
19730 let sheet_id = self
19731 .graph
19732 .sheet_id(sheet_name)
19733 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19734 let sheet_index = self
19735 .graph
19736 .sheet_reg()
19737 .active_position_by_id(sheet_id)
19738 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19739 Ok((sheet_id, sheet_index))
19740 };
19741
19742 let cell_info =
19743 |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
19744 let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
19745 let row0 = row
19746 .checked_sub(1)
19747 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19748 let col0 = col
19749 .checked_sub(1)
19750 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19751 Ok(ReferenceInfo {
19752 first_sheet_index: Some(sheet_index),
19753 sheet_count: Some(1),
19754 first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
19755 })
19756 };
19757
19758 let range_info = |sheet_name: &str,
19759 start_row: Option<u32>,
19760 start_col: Option<u32>|
19761 -> Result<ReferenceInfo, ExcelError> {
19762 let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
19763 let row = start_row.unwrap_or(1);
19764 let col = start_col.unwrap_or(1);
19765 let row0 = row
19766 .checked_sub(1)
19767 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19768 let col0 = col
19769 .checked_sub(1)
19770 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19771 Ok(ReferenceInfo {
19772 first_sheet_index: Some(sheet_index),
19773 sheet_count: Some(1),
19774 first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
19775 })
19776 };
19777
19778 let info = match reference {
19779 ReferenceType::Cell {
19780 sheet, row, col, ..
19781 } => {
19782 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19783 cell_info(sheet_name, *row, *col)?
19784 }
19785 ReferenceType::Range {
19786 sheet,
19787 start_row,
19788 start_col,
19789 ..
19790 } => {
19791 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19792 range_info(sheet_name, *start_row, *start_col)?
19793 }
19794 ReferenceType::Cell3D {
19795 sheet_first,
19796 sheet_last,
19797 row,
19798 col,
19799 ..
19800 } => {
19801 let first = cell_info(sheet_first, *row, *col)?;
19802 ReferenceInfo {
19803 first_sheet_index: first.first_sheet_index,
19804 sheet_count: self
19805 .graph
19806 .sheet_reg()
19807 .active_span_len(sheet_first, sheet_last),
19808 first_cell: first.first_cell,
19809 }
19810 }
19811 ReferenceType::Range3D {
19812 sheet_first,
19813 sheet_last,
19814 start_row,
19815 start_col,
19816 ..
19817 } => {
19818 let first = range_info(sheet_first, *start_row, *start_col)?;
19819 ReferenceInfo {
19820 first_sheet_index: first.first_sheet_index,
19821 sheet_count: self
19822 .graph
19823 .sheet_reg()
19824 .active_span_len(sheet_first, sheet_last),
19825 first_cell: first.first_cell,
19826 }
19827 }
19828 ReferenceType::NamedRange(name) => {
19829 let current_id = self
19830 .graph
19831 .sheet_id(current_sheet)
19832 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19833 let named = self
19834 .graph
19835 .resolve_name_entry(name, current_id)
19836 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19837 match &named.definition {
19838 NamedDefinition::Cell(cell) => ReferenceInfo {
19839 first_sheet_index: self
19840 .graph
19841 .sheet_reg()
19842 .active_position_by_id(cell.sheet_id),
19843 sheet_count: Some(1),
19844 first_cell: Some(*cell),
19845 },
19846 NamedDefinition::Range(range) => ReferenceInfo {
19847 first_sheet_index: self
19848 .graph
19849 .sheet_reg()
19850 .active_position_by_id(range.start.sheet_id),
19851 sheet_count: Some(1),
19852 first_cell: Some(range.start),
19853 },
19854 NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
19855 ReferenceInfo {
19856 first_sheet_index: None,
19857 sheet_count: None,
19858 first_cell: None,
19859 }
19860 }
19861 }
19862 }
19863 ReferenceType::Table(tref) => {
19864 let table = self
19865 .graph
19866 .resolve_table_entry(&tref.name)
19867 .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19868 ReferenceInfo {
19869 first_sheet_index: self
19870 .graph
19871 .sheet_reg()
19872 .active_position_by_id(table.range.start.sheet_id),
19873 sheet_count: Some(1),
19874 first_cell: Some(table.range.start),
19875 }
19876 }
19877 ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
19878 };
19879
19880 Ok(Some(info))
19881 }
19882
19883 fn resolve_spill_reference(
19884 &self,
19885 anchor: &ReferenceType,
19886 current_sheet: &str,
19887 ) -> Result<ReferenceType, ExcelError> {
19888 let no_spill = || {
19889 ExcelError::new(ExcelErrorKind::Ref)
19890 .with_message("Spill reference anchor has no current spill")
19891 };
19892 let mut qualify = true;
19895 let anchor_cell = match anchor {
19896 ReferenceType::Cell {
19897 sheet, row, col, ..
19898 } => {
19899 qualify = sheet.is_some();
19900 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19901 let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(no_spill)?;
19902 if *row == 0 || *col == 0 {
19903 return Err(no_spill());
19904 }
19905 CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true))
19906 }
19907 ReferenceType::NamedRange(name) => {
19908 let current_id = self.graph.sheet_id(current_sheet).ok_or_else(no_spill)?;
19909 let named = self
19910 .graph
19911 .resolve_name_entry(name, current_id)
19912 .ok_or_else(no_spill)?;
19913 match &named.definition {
19914 NamedDefinition::Cell(cell) => *cell,
19915 NamedDefinition::Range(range)
19916 if range.start.sheet_id == range.end.sheet_id
19917 && range.start.coord.row() == range.end.coord.row()
19918 && range.start.coord.col() == range.end.coord.col() =>
19919 {
19920 range.start
19921 }
19922 _ => {
19923 return Err(ExcelError::new(ExcelErrorKind::Ref)
19924 .with_message("Spill reference name must refer to a single cell"));
19925 }
19926 }
19927 }
19928 _ => {
19929 return Err(ExcelError::new(ExcelErrorKind::Ref)
19930 .with_message("Spill reference operand must be a single cell"));
19931 }
19932 };
19933 let vertex = self
19934 .graph
19935 .get_vertex_id_for_address(&anchor_cell)
19936 .ok_or_else(no_spill)?;
19937 if self.graph.fixed_single_arrays.contains(&vertex)
19940 || self.graph.fixed_array_shapes.contains_key(&vertex)
19941 {
19942 return Err(ExcelError::new(ExcelErrorKind::Ref)
19943 .with_message("Spill reference to a fixed array formula"));
19944 }
19945 let (first, last) = match self.graph.spill_extent_for_anchor(vertex) {
19946 Some(extent) => extent,
19947 None if !self.graph.spill_registry_has_anchor(vertex)
19951 && self.graph.is_current_declared_dynamic_anchor(vertex)
19952 && self.declared_anchor_holds_scalar(anchor_cell) =>
19953 {
19954 (anchor_cell, anchor_cell)
19955 }
19956 None => return Err(no_spill()),
19957 };
19958 let sheet_name = qualify.then(|| self.graph.sheet_name(first.sheet_id).to_string());
19959 let (sr, sc) = (first.coord.row() + 1, first.coord.col() + 1);
19960 let (er, ec) = (last.coord.row() + 1, last.coord.col() + 1);
19961 Ok(if sr == er && sc == ec {
19963 ReferenceType::cell_with_abs(sheet_name, sr, sc, true, true)
19964 } else {
19965 ReferenceType::Range {
19966 sheet: sheet_name,
19967 start_row: Some(sr),
19968 start_col: Some(sc),
19969 end_row: Some(er),
19970 end_col: Some(ec),
19971 start_row_abs: true,
19972 start_col_abs: true,
19973 end_row_abs: true,
19974 end_col_abs: true,
19975 }
19976 })
19977 }
19978
19979 fn has_formula_at_cell(&self, cell: CellRef) -> Result<bool, ExcelError> {
19980 let sheet_name = self.graph.sheet_name(cell.sheet_id);
19981 if sheet_name.is_empty() {
19982 return Err(ExcelError::new(ExcelErrorKind::Ref));
19983 }
19984 if self.staged_formulas.get(sheet_name).is_some_and(|entries| {
19985 entries
19986 .get(cell.coord.row() + 1, cell.coord.col() + 1)
19987 .is_some()
19988 }) {
19989 return Ok(true);
19990 }
19991 Ok(self
19992 .graph
19993 .get_vertex_for_cell(&cell)
19994 .is_some_and(|vertex| self.graph.is_live_formula_vertex(vertex)))
19995 }
19996
19997 fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
19998 let sheet_name = self.graph.sheet_name(cell.sheet_id);
19999 if sheet_name.is_empty() {
20000 return Err(ExcelError::new(ExcelErrorKind::Ref));
20001 }
20002 let row = cell.coord.row() + 1;
20003 let col = cell.coord.col() + 1;
20004
20005 if let Some(entries) = self.staged_formulas.get(sheet_name)
20006 && let Some(text) = entries.get(row, col)
20007 {
20008 return Ok(Some(if text.starts_with('=') {
20009 text.to_owned()
20010 } else {
20011 format!("={text}")
20012 }));
20013 }
20014
20015 let Some((Some(ast), _)) = self.get_cell(sheet_name, row, col) else {
20016 return Ok(None);
20017 };
20018 Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
20019 }
20020
20021 fn used_rows_for_columns(
20022 &self,
20023 sheet: &str,
20024 start_col: u32,
20025 end_col: u32,
20026 ) -> Option<(u32, u32)> {
20027 let sheet_id = self.graph.sheet_id(sheet)?;
20029 let snap = self.data_snapshot_id();
20030 if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
20031 guard
20032 .as_ref()
20033 .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
20034 }) {
20035 return cached;
20036 }
20037
20038 let arrow_bounds = self
20039 .sheet_store()
20040 .sheet(sheet)
20041 .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
20042 let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
20043 let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
20044 Some(bounds)
20045 } else {
20046 let sc0 = start_col.saturating_sub(1);
20047 let ec0 = end_col.saturating_sub(1);
20048 self.graph
20049 .used_row_bounds_for_columns(sheet_id, sc0, ec0)
20050 .map(|(a0, b0)| (a0 + 1, b0 + 1))
20051 };
20052
20053 if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
20054 guard
20055 .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
20056 .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
20057 }
20058
20059 computed
20060 }
20061
20062 fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
20063 let sheet_id = self.graph.sheet_id(sheet)?;
20065 let snap = self.data_snapshot_id();
20066 if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
20067 guard
20068 .as_ref()
20069 .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
20070 }) {
20071 return cached;
20072 }
20073
20074 let arrow_bounds = self
20075 .sheet_store()
20076 .sheet(sheet)
20077 .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
20078 let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
20079 let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
20080 Some(bounds)
20081 } else {
20082 let sr0 = start_row.saturating_sub(1);
20083 let er0 = end_row.saturating_sub(1);
20084 self.graph
20085 .used_col_bounds_for_rows(sheet_id, sr0, er0)
20086 .map(|(a0, b0)| (a0 + 1, b0 + 1))
20087 };
20088
20089 if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
20090 guard
20091 .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
20092 .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
20093 }
20094
20095 computed
20096 }
20097
20098 fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
20099 let _ = self.graph.sheet_id(sheet)?;
20100 Some((1_048_576, 16_384)) }
20104
20105 fn data_snapshot_id(&self) -> u64 {
20106 self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
20107 }
20108
20109 fn backend_caps(&self) -> crate::traits::BackendCaps {
20110 crate::traits::BackendCaps {
20111 streaming: true,
20112 used_region: true,
20113 write: false,
20114 tables: false,
20115 async_stream: false,
20116 }
20117 }
20118
20119 fn build_lookup_index(
20120 &self,
20121 view: &RangeView<'_>,
20122 axis: LookupAxis,
20123 ) -> Option<Arc<LookupIndex>> {
20124 self.build_lookup_index_impl(view, axis)
20125 }
20126
20127 fn date_system(&self) -> crate::engine::DateSystem {
20130 self.config.date_system
20131 }
20132 fn resolve_range_view<'c>(
20134 &'c self,
20135 reference: &ReferenceType,
20136 current_sheet: &str,
20137 ) -> Result<RangeView<'c>, ExcelError> {
20138 match reference {
20139 ReferenceType::External(ext) => {
20140 let name = ext.raw.as_str();
20141 match ext.kind {
20142 formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
20143 let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
20144 return Err(ExcelError::new(ExcelErrorKind::Name)
20145 .with_message(format!("Undefined name: {name}")));
20146 };
20147 let version = source
20148 .version
20149 .or_else(|| self.resolver.source_scalar_version(name));
20150 let v = self.resolve_source_scalar_cached(name, version)?;
20151 Ok(RangeView::from_owned_rows(
20152 vec![vec![v]],
20153 self.config.date_system,
20154 ))
20155 }
20156 formualizer_parse::parser::ExternalRefKind::Range { .. } => {
20157 let Some(source) = self.graph.resolve_source_table_entry(name) else {
20158 let kind =
20162 if crate::engine::refs::unbound_external_range_defers(&ext.kind) {
20163 ExcelErrorKind::Ref
20164 } else {
20165 ExcelErrorKind::Name
20166 };
20167 return Err(ExcelError::new(kind)
20168 .with_message(format!("Undefined table: {name}")));
20169 };
20170 let version = source
20171 .version
20172 .or_else(|| self.resolver.source_table_version(name));
20173 let key = (name.to_string(), version);
20174 if let Ok(g) = self.source_cache.read()
20175 && let Some(view) = g.external_views.get(&key)
20176 {
20177 return Ok(view.clone());
20178 }
20179 let table = self.resolve_source_table_cached(name, version)?;
20180 let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
20181 let Ok(mut g) = self.source_cache.write() else {
20184 return self.source_table_to_range_view(table.as_ref(), &spec);
20185 };
20186 if let Some(view) = g.external_views.get(&key) {
20187 return Ok(view.clone());
20188 }
20189 let view = self.source_table_to_range_view(table.as_ref(), &spec)?;
20190 g.external_views.insert(key, view.clone());
20191 Ok(view)
20192 }
20193 }
20194 }
20195 ReferenceType::Range { .. } => {
20196 let shared = self.resolve_shared_ref(reference, current_sheet)?;
20197 let formualizer_common::SheetRef::Range(range) = shared else {
20198 return Err(ExcelError::new(ExcelErrorKind::Ref));
20199 };
20200 let sheet_id = match range.sheet {
20203 formualizer_common::SheetLocator::Id(id) => id,
20204 formualizer_common::SheetLocator::Current
20205 | formualizer_common::SheetLocator::Name(_) => {
20206 return Err(ExcelError::new(ExcelErrorKind::Ref));
20207 }
20208 };
20209 let bounded_range = if range.start_row.is_some()
20210 && range.start_col.is_some()
20211 && range.end_row.is_some()
20212 && range.end_col.is_some()
20213 {
20214 Some(RangeRef::try_from_shared(range.as_ref())?)
20215 } else {
20216 None
20217 };
20218
20219 let sr = bounded_range
20220 .as_ref()
20221 .map(|r| r.start.coord.row() + 1)
20222 .or_else(|| range.start_row.map(|b| b.index + 1));
20223 let sc = bounded_range
20224 .as_ref()
20225 .map(|r| r.start.coord.col() + 1)
20226 .or_else(|| range.start_col.map(|b| b.index + 1));
20227 let er = bounded_range
20228 .as_ref()
20229 .map(|r| r.end.coord.row() + 1)
20230 .or_else(|| range.end_row.map(|b| b.index + 1));
20231 let ec = bounded_range
20232 .as_ref()
20233 .map(|r| r.end.coord.col() + 1)
20234 .or_else(|| range.end_col.map(|b| b.index + 1));
20235
20236 self.resolve_range_coordinates(sheet_id, sr, sc, er, ec)
20237 }
20238 ReferenceType::Cell { .. } => {
20239 let shared = self.resolve_shared_ref(reference, current_sheet)?;
20240 let formualizer_common::SheetRef::Cell(cell) = shared else {
20241 return Err(ExcelError::new(ExcelErrorKind::Ref));
20242 };
20243 let addr = CellRef::try_from_shared(cell)?;
20244 let sheet_id = addr.sheet_id;
20245 let sheet_name = self.graph.sheet_name(sheet_id);
20246 let row = addr.coord.row() + 1;
20247 let col = addr.coord.col() + 1;
20248
20249 if self.force_materialize_range_views {
20250 let v = self
20251 .read_cell_value(sheet_name, row, col)
20252 .unwrap_or(LiteralValue::Empty);
20253 return Ok(RangeView::from_owned_rows(
20254 vec![vec![v]],
20255 self.config.date_system,
20256 ));
20257 }
20258
20259 if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
20260 let r0 = row.saturating_sub(1) as usize;
20261 let c0 = col.saturating_sub(1) as usize;
20262 let rv = asheet.range_view(r0, c0, r0, c0);
20263 Ok(rv)
20264 } else {
20265 let v = self
20266 .read_cell_value(sheet_name, row, col)
20267 .unwrap_or(LiteralValue::Empty);
20268 Ok(RangeView::from_owned_rows(
20269 vec![vec![v]],
20270 self.config.date_system,
20271 ))
20272 }
20273 }
20274 ReferenceType::NamedRange(name) => {
20275 if let Some(current_id) = self.graph.sheet_id(current_sheet)
20276 && let Some(named) = self.graph.resolve_name_entry(name, current_id)
20277 {
20278 match &named.definition {
20279 NamedDefinition::Cell(cell_ref) => {
20280 let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
20281 if self.force_materialize_range_views {
20282 let v = self
20283 .read_cell_value(
20284 sheet_name,
20285 cell_ref.coord.row() + 1,
20286 cell_ref.coord.col() + 1,
20287 )
20288 .unwrap_or(LiteralValue::Empty);
20289 return Ok(RangeView::from_owned_rows(
20290 vec![vec![v]],
20291 self.config.date_system,
20292 ));
20293 } else {
20294 let asheet = self
20295 .sheet_store()
20296 .sheet(sheet_name)
20297 .expect("Arrow sheet missing for named cell");
20298 let r0 = cell_ref.coord.row() as usize;
20299 let c0 = cell_ref.coord.col() as usize;
20300 let rv = asheet.range_view(r0, c0, r0, c0);
20301 return Ok(rv);
20302 }
20303 }
20304 NamedDefinition::Range(range_ref) => {
20305 let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
20306 let sr = range_ref.start.coord.row() + 1;
20307 let sc = range_ref.start.coord.col() + 1;
20308 let er = range_ref.end.coord.row() + 1;
20309 let ec = range_ref.end.coord.col() + 1;
20310 if self.force_materialize_range_views {
20311 let h = (er.saturating_sub(sr) + 1) as u64;
20312 let w = (ec.saturating_sub(sc) + 1) as u64;
20313 let cell_count = h.saturating_mul(w);
20314 if cell_count <= self.config.spill.max_spill_cells as u64 {
20315 let mut rows: Vec<Vec<LiteralValue>> =
20316 Vec::with_capacity(h as usize);
20317 for r in sr..=er {
20318 let mut rowv: Vec<LiteralValue> =
20319 Vec::with_capacity(w as usize);
20320 for c in sc..=ec {
20321 rowv.push(
20322 self.read_cell_value(sheet_name, r, c)
20323 .unwrap_or(LiteralValue::Empty),
20324 );
20325 }
20326 rows.push(rowv);
20327 }
20328 return Ok(RangeView::from_owned_rows(
20329 rows,
20330 self.config.date_system,
20331 ));
20332 }
20333 }
20334 let asheet = self
20335 .sheet_store()
20336 .sheet(sheet_name)
20337 .expect("Arrow sheet missing for named range");
20338 let sr0 = range_ref.start.coord.row() as usize;
20339 let sc0 = range_ref.start.coord.col() as usize;
20340 let er0 = range_ref.end.coord.row() as usize;
20341 let ec0 = range_ref.end.coord.col() as usize;
20342 let rv = asheet.range_view(sr0, sc0, er0, ec0);
20343 return Ok(rv);
20344 }
20345 NamedDefinition::Literal(v) => {
20346 return Ok(RangeView::from_owned_rows(
20347 vec![vec![v.clone()]],
20348 self.config.date_system,
20349 ));
20350 }
20351 NamedDefinition::Formula { ast, .. } => {
20352 if let ASTNodeType::Reference {
20355 reference: external @ ReferenceType::External(_),
20356 ..
20357 } = &ast.node_type
20358 {
20359 return self.resolve_range_view(external, current_sheet);
20360 }
20361 if let Some(value) = self.graph.get_value(named.vertex) {
20362 return Ok(RangeView::from_owned_rows(
20363 vec![vec![value]],
20364 self.config.date_system,
20365 ));
20366 }
20367 }
20368 }
20369 }
20370
20371 if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
20372 let version = source
20373 .version
20374 .or_else(|| self.resolver.source_scalar_version(name));
20375 let v = self.resolve_source_scalar_cached(name, version)?;
20376 return Ok(RangeView::from_owned_rows(
20377 vec![vec![v]],
20378 self.config.date_system,
20379 ));
20380 }
20381
20382 let data = self.resolver.resolve_named_range_reference(name)?;
20383 Ok(RangeView::from_owned_rows(data, self.config.date_system))
20384 }
20385 ReferenceType::Table(tref) => {
20386 if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
20387 let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
20388 let asheet = self
20389 .sheet_store()
20390 .sheet(sheet_name)
20391 .expect("Arrow sheet missing for table reference");
20392
20393 let sr0 = table.range.start.coord.row() as usize;
20394 let sc0 = table.range.start.coord.col() as usize;
20395 let er0 = table.range.end.coord.row() as usize;
20396 let ec0 = table.range.end.coord.col() as usize;
20397
20398 let has_totals = table.totals_row;
20399 let has_headers = table.header_row;
20400 let data_sr = if table.empty_body {
20401 er0.saturating_add(1)
20402 } else if has_headers {
20403 sr0.saturating_add(1)
20404 } else {
20405 sr0
20406 };
20407 let data_er = if has_totals {
20408 er0.saturating_sub(1)
20409 } else {
20410 er0
20411 };
20412
20413 let select = |sr: usize, sc: usize, er: usize, ec: usize| {
20414 if sr > er || sc > ec {
20415 asheet.range_view(1, 1, 0, 0)
20416 } else {
20417 asheet.range_view(sr, sc, er, ec)
20418 }
20419 };
20420
20421 let av = match &tref.specifier {
20422 None => {
20423 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20424 "Table reference without specifier is unsupported".to_string(),
20425 ));
20426 }
20427 Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
20428 let Some(idx) = table.col_index(col) else {
20429 return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20430 "Column refers to unknown table column".to_string(),
20431 ));
20432 };
20433 let c0 = sc0 + idx;
20434 select(data_sr, c0, data_er, c0)
20435 }
20436 Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
20437 start,
20438 end,
20439 )) => {
20440 let Some(si) = table.col_index(start) else {
20441 return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20442 "Column range refers to unknown column(s)".to_string(),
20443 ));
20444 };
20445 let Some(ei) = table.col_index(end) else {
20446 return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20447 "Column range refers to unknown column(s)".to_string(),
20448 ));
20449 };
20450 let (mut a, mut b) = (si, ei);
20451 if a > b {
20452 std::mem::swap(&mut a, &mut b);
20453 }
20454 let c_start = sc0 + a;
20455 let c_end = sc0 + b;
20456 select(data_sr, c_start, data_er, c_end)
20457 }
20458 Some(formualizer_parse::parser::TableSpecifier::All)
20459 | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20460 formualizer_parse::parser::SpecialItem::All,
20461 )) => select(sr0, sc0, er0, ec0),
20462 Some(formualizer_parse::parser::TableSpecifier::Data)
20463 | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20464 formualizer_parse::parser::SpecialItem::Data,
20465 )) => select(data_sr, sc0, data_er, ec0),
20466 Some(formualizer_parse::parser::TableSpecifier::Headers)
20467 | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20468 formualizer_parse::parser::SpecialItem::Headers,
20469 )) => {
20470 if !has_headers {
20471 asheet.range_view(1, 1, 0, 0)
20472 } else {
20473 select(sr0, sc0, sr0, ec0)
20474 }
20475 }
20476 Some(formualizer_parse::parser::TableSpecifier::Totals)
20477 | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20478 formualizer_parse::parser::SpecialItem::Totals,
20479 )) => {
20480 if !has_totals {
20481 asheet.range_view(1, 1, 0, 0)
20482 } else {
20483 select(er0, sc0, er0, ec0)
20484 }
20485 }
20486 Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20487 formualizer_parse::parser::SpecialItem::ThisRow,
20488 )) => {
20489 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20490 "@ (This Row) requires table-aware context; not yet supported"
20491 .to_string(),
20492 ));
20493 }
20494 Some(formualizer_parse::parser::TableSpecifier::Row(_))
20495 | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
20496 return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20497 "Complex structured references not yet supported".to_string(),
20498 ));
20499 }
20500 };
20501
20502 return Ok(av);
20503 }
20504
20505 if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
20506 let version = source
20507 .version
20508 .or_else(|| self.resolver.source_table_version(&tref.name));
20509 let table = self.resolve_source_table_cached(&tref.name, version)?;
20510 return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
20511 }
20512
20513 let boxed = self
20518 .resolve_range_like(&ReferenceType::Table(tref.clone()))
20519 .map_err(|e| {
20520 if e.kind == ExcelErrorKind::NImpl {
20521 ExcelError::new(ExcelErrorKind::Name)
20522 .with_message(format!("Unknown table: {}", tref.name))
20523 } else {
20524 e
20525 }
20526 })?;
20527 let owned = boxed.materialise().into_owned();
20528 Ok(RangeView::from_owned_rows(owned, self.config.date_system))
20529 }
20530 ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
20531 Err(ExcelError::new(ExcelErrorKind::NImpl)
20532 .with_message("3D references are not yet supported".to_string()))
20533 }
20534 }
20535 }
20536
20537 fn resolve_cell_format(
20538 &self,
20539 sheet: Option<&str>,
20540 row: u32,
20541 col: u32,
20542 current_sheet: &str,
20543 ) -> Option<crate::format::FormatId> {
20544 self.effective_format_id(sheet.unwrap_or(current_sheet), row, col)
20545 }
20546
20547 fn format_class(
20548 &self,
20549 format: crate::format::FormatId,
20550 ) -> Option<formualizer_common::numfmt::FormatClass> {
20551 self.format_registry.class(format).cloned()
20552 }
20553
20554 fn record_cell_derived_format(
20555 &self,
20556 sheet: &str,
20557 row: u32,
20558 col: u32,
20559 format: Option<crate::format::FormatId>,
20560 ) {
20561 if let Some(sheet_id) = self.graph.sheet_id(sheet) {
20562 let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
20563 self.record_derived_format_at(cell, format);
20564 }
20565 }
20566
20567 fn resolve_cell_reference_value(
20568 &self,
20569 sheet: Option<&str>,
20570 row: u32,
20571 col: u32,
20572 current_sheet: &str,
20573 ) -> Result<LiteralValue, ExcelError> {
20574 let sheet_name = sheet.unwrap_or(current_sheet);
20575 if self.graph.sheet_id(sheet_name).is_none() {
20576 return Err(ExcelError::new(ExcelErrorKind::Ref));
20577 }
20578 Ok(self
20579 .read_cell_value(sheet_name, row, col)
20580 .unwrap_or(LiteralValue::Empty))
20581 }
20582
20583 fn resolve_cell_reference_value_formatted(
20584 &self,
20585 sheet: Option<&str>,
20586 row: u32,
20587 col: u32,
20588 current_sheet: &str,
20589 ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
20590 let sheet_name = sheet.unwrap_or(current_sheet);
20593 let Some(sheet_id) = self.graph.sheet_id(sheet_name) else {
20594 return Err(ExcelError::new(ExcelErrorKind::Ref));
20595 };
20596 let asheet = self.arrow_sheets.sheet(sheet_name);
20597 Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
20598 }
20599
20600 fn build_criteria_mask(
20601 &self,
20602 view: &RangeView<'_>,
20603 col_in_view: usize,
20604 pred: &crate::args::CriteriaPredicate,
20605 ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
20606 #[cfg(any(test, feature = "test-support"))]
20607 criteria_mask_test_hooks::note_mask(view.dims().0);
20608 if view.dims().1 == 0 {
20609 return None;
20610 }
20611 let sheet_rows = view.sheet().nrows as usize;
20614 if sheet_rows == 0 || view.start_row() >= sheet_rows {
20615 return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
20616 }
20617 self.criteria_mask_cache.get_or_build(
20618 view,
20619 col_in_view,
20620 pred,
20621 (
20622 self.recalc_epoch,
20623 self.topology_epoch,
20624 self.graph.topology_revision(),
20625 ),
20626 || compute_criteria_mask(view, col_in_view, pred),
20627 )
20628 }
20629
20630 fn build_row_visibility_mask(
20631 &self,
20632 view: &RangeView<'_>,
20633 mode: VisibilityMaskMode,
20634 ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
20635 self.build_row_visibility_mask_for_view(view, mode)
20636 }
20637
20638 fn nested_subtotal_cells(
20639 &self,
20640 view: &RangeView<'_>,
20641 include_aggregate: bool,
20642 ) -> Option<Vec<(usize, usize, usize)>> {
20643 self.nested_subtotal_cells_for_view(view, include_aggregate)
20644 }
20645}
20646
20647impl<R> Engine<R>
20648where
20649 R: EvaluationContext,
20650{
20651 fn clear_spill_projection_and_mirror(
20652 &mut self,
20653 anchor_vertex: VertexId,
20654 delta: Option<&mut DeltaCollector>,
20655 ) {
20656 let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
20657 if spill_cells.is_empty() {
20658 return;
20659 }
20660
20661 if let Some(delta) = delta
20662 && delta.mode != DeltaMode::Off
20663 {
20664 let empty = LiteralValue::Empty;
20665 for cell in spill_cells.iter() {
20666 let sheet_name = self.graph.sheet_name(cell.sheet_id);
20667 let old = self
20668 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20669 .unwrap_or(LiteralValue::Empty);
20670 if old != empty {
20671 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20672 }
20673 }
20674 }
20675
20676 self.graph.clear_spill_region(anchor_vertex);
20677 self.freshness_note_spill_change(&spill_cells, &[]);
20678 if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
20679 self.record_structural_change(scope);
20680 }
20681
20682 if self.config.arrow_storage_enabled
20683 && self.config.delta_overlay_enabled
20684 && self.config.write_formula_overlay_enabled
20685 {
20686 let empty = LiteralValue::Empty;
20687 for cell in spill_cells.iter() {
20688 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20689 self.mirror_value_to_computed_overlay(
20690 &sheet_name,
20691 cell.coord.row() + 1,
20692 cell.coord.col() + 1,
20693 &empty,
20694 );
20695 }
20696 }
20697 }
20698
20699 fn apply_cycle_outcome(
20713 &mut self,
20714 cycle: &[VertexId],
20715 mut delta: Option<&mut DeltaCollector>,
20716 dirty_filter: Option<&FxHashSet<VertexId>>,
20717 ) -> usize {
20718 let circ_error = LiteralValue::Error(
20719 ExcelError::new(ExcelErrorKind::Circ)
20720 .with_message("Circular dependency detected".to_string()),
20721 );
20722 let mut stamped = 0usize;
20723 for &vertex_id in cycle {
20724 if let Some(filter) = dirty_filter
20725 && !filter.contains(&vertex_id)
20726 {
20727 continue;
20728 }
20729 self.stamp_cycle_error(vertex_id, &circ_error, delta.as_deref_mut());
20730 stamped += 1;
20731 }
20732 stamped
20733 }
20734
20735 fn stamp_cycle_error(
20750 &mut self,
20751 vertex_id: VertexId,
20752 circ_error: &LiteralValue,
20753 mut delta: Option<&mut DeltaCollector>,
20754 ) {
20755 if self.graph.spill_registry_has_anchor(vertex_id) {
20757 self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
20758 }
20759 self.spill_mgr.release_owner(vertex_id);
20762
20763 if let Some(d) = delta
20765 && d.mode != DeltaMode::Off
20766 && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
20767 {
20768 let sheet_name = self.graph.sheet_name(cell.sheet_id);
20769 let old = self
20770 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20771 .unwrap_or(LiteralValue::Empty);
20772 if old != *circ_error {
20773 d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20774 }
20775 }
20776
20777 self.graph.update_vertex_value_ref(vertex_id, circ_error);
20778 self.mirror_vertex_value_to_overlay(vertex_id, circ_error);
20779 }
20780
20781 fn handle_cycle_unit(
20794 &mut self,
20795 cycle: &[VertexId],
20796 mut delta: Option<&mut DeltaCollector>,
20797 dirty_filter: Option<&FxHashSet<VertexId>>,
20798 cancel_flag: Option<&AtomicBool>,
20799 ) -> Result<usize, ExcelError> {
20800 self.resource_checkpoint(cycle.len() as u64)?;
20801 match self.config.cycle.detection {
20802 CycleDetection::Static => {
20803 Ok(self.apply_cycle_outcome(cycle, delta.as_deref_mut(), dirty_filter))
20804 }
20805 CycleDetection::Runtime => {
20806 if let Some(filter) = dirty_filter
20807 && !cycle.iter().any(|v| filter.contains(v))
20808 {
20809 return Ok(0);
20810 }
20811 self.evaluate_scc_unit(cycle, delta, cancel_flag)
20815 }
20816 }
20817 }
20818
20819 pub(crate) fn evaluate_scc_unit(
20839 &mut self,
20840 cycle: &[VertexId],
20841 mut delta: Option<&mut DeltaCollector>,
20842 cancel_flag: Option<&AtomicBool>,
20843 ) -> Result<usize, ExcelError> {
20844 struct SccMember {
20845 vertex: VertexId,
20846 cell: Option<CellRef>,
20847 }
20848
20849 let task_start = crate::instant::FzInstant::now();
20850
20851 let mut cell_members: Vec<(VertexId, CellRef)> = Vec::new();
20856 let mut name_members: Vec<(VertexId, String)> = Vec::new();
20857 let mut other_members: Vec<VertexId> = Vec::new();
20858 for &v in cycle {
20859 match self.graph.get_vertex_kind(v) {
20860 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
20861 match self.graph.get_cell_ref(v) {
20862 Some(cell) => cell_members.push((v, cell)),
20863 None => other_members.push(v),
20864 }
20865 }
20866 VertexKind::NamedScalar | VertexKind::NamedArray => {
20867 match self.graph.name_key_for_vertex(v) {
20868 Some(key) => name_members.push((v, key)),
20869 None => other_members.push(v),
20870 }
20871 }
20872 _ => other_members.push(v),
20873 }
20874 }
20875 cell_members.sort_unstable_by_key(|(_, c)| (c.sheet_id, c.coord.row(), c.coord.col()));
20876 name_members.sort_unstable_by(|(av, ak), (bv, bk)| ak.cmp(bk).then(av.cmp(bv)));
20877 other_members.sort_unstable();
20878
20879 let cell_refs: Vec<CellRef> = cell_members.iter().map(|(_, c)| *c).collect();
20880 let name_keys: Vec<String> = name_members.iter().map(|(_, k)| k.clone()).collect();
20881 let mut members: Vec<SccMember> = Vec::with_capacity(cycle.len());
20882 for (v, c) in &cell_members {
20883 members.push(SccMember {
20884 vertex: *v,
20885 cell: Some(*c),
20886 });
20887 }
20888 for (v, _) in &name_members {
20889 members.push(SccMember {
20890 vertex: *v,
20891 cell: None,
20892 });
20893 }
20894 for v in &other_members {
20895 members.push(SccMember {
20896 vertex: *v,
20897 cell: None,
20898 });
20899 }
20900 let n = members.len();
20901 let recordable = cell_refs.len() + name_keys.len();
20904
20905 let circ_error = LiteralValue::Error(
20906 ExcelError::new(ExcelErrorKind::Circ)
20907 .with_message("Circular dependency detected".to_string()),
20908 );
20909
20910 if !self.iterative_state_values.is_empty() {
20921 let restore: Vec<(VertexId, LiteralValue)> = members
20922 .iter()
20923 .filter_map(|m| {
20924 let cell = m.cell?;
20925 let persisted = self.iterative_state_values.get(&m.vertex)?;
20926 let sheet_name = self.graph.sheet_name(cell.sheet_id);
20927 let overlay = self
20928 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20929 .unwrap_or(LiteralValue::Empty);
20930 if matches!(overlay, LiteralValue::Empty) {
20931 Some((m.vertex, persisted.clone()))
20932 } else {
20933 None
20934 }
20935 })
20936 .collect();
20937 for (vertex, value) in restore {
20938 self.mirror_vertex_value_to_overlay(vertex, &value);
20939 }
20940 }
20941
20942 let mut snapshot: Vec<LiteralValue> = members
20945 .iter()
20946 .map(|m| match m.cell {
20947 Some(cell) => {
20948 let sheet_name = self.graph.sheet_name(cell.sheet_id);
20949 self.read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20950 .unwrap_or(LiteralValue::Empty)
20951 }
20952 None => self
20953 .graph
20954 .get_value(m.vertex)
20955 .unwrap_or(LiteralValue::Empty),
20956 })
20957 .collect();
20958
20959 if let Some(state) = &mut self.resumable {
20960 let mut key = cycle.to_vec();
20964 key.sort_unstable();
20965 if cycle.iter().any(|v| state.new_cycle_members.contains(v)) {
20966 state.cycle_seeds.retain(|old, _| {
20970 state.original_cycles.contains(old)
20971 || *old == key
20972 || !cycle.iter().any(|v| old.binary_search(v).is_ok())
20973 });
20974 }
20975 if !state.original_cycles.contains(&key) {
20976 state.new_cycle_members.extend(cycle.iter().copied());
20977 }
20978 let seed_memory_changed = !state.original_cycles.contains(&key);
20979 let seeds = state.cycle_seeds.entry(key).or_default();
20980 for (member, value) in members.iter().zip(&mut snapshot) {
20981 let seed = seeds.entry(member.vertex).or_insert_with(|| value.clone());
20982 *value = seed.clone();
20983 }
20984 if seed_memory_changed {
20985 state.refresh_seed_retained_bytes();
20986 }
20987 for (member, value) in members.iter().zip(&snapshot) {
20988 self.graph.update_vertex_value_ref(member.vertex, value);
20989 self.mirror_vertex_value_to_overlay(member.vertex, value);
20990 }
20991 }
20992
20993 let mut excluded = vec![false; n];
20998 let mut last_value = snapshot.clone();
20999 let mut stamped = 0usize;
21000 for (i, m) in members.iter().enumerate() {
21001 match self.graph.get_vertex_kind(m.vertex) {
21002 VertexKind::FormulaArray => {
21003 self.stamp_cycle_error(m.vertex, &circ_error, delta.as_deref_mut());
21007 excluded[i] = true;
21008 last_value[i] = circ_error.clone();
21009 stamped += 1;
21010 }
21011 VertexKind::FormulaScalar | VertexKind::NamedScalar | VertexKind::NamedArray => {}
21012 _ => excluded[i] = true,
21013 }
21014 }
21015
21016 let collector = LiveEdgeCollector::new_with_names(&cell_refs, &name_keys);
21017
21018 let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
21020 let mut pos: Vec<i64> = vec![-1; n];
21022 let mut changed = vec![false; n];
21024
21025 macro_rules! run_member {
21028 ($i:expr) => {{
21029 let i: usize = $i;
21030 let m = &members[i];
21031 if i < recordable {
21032 collector.set_current(i as u32);
21033 }
21034 let value = {
21035 let ctx = RecordingContext::new(&*self, &collector);
21036 match self.evaluate_vertex_recorded(m.vertex, &ctx, &collector) {
21037 Ok(v) => v,
21038 Err(e) => LiteralValue::Error(e),
21039 }
21040 };
21041 let is_cell_formula = m.cell.is_some();
21042 if is_cell_formula && matches!(value, LiteralValue::Array(_)) {
21043 self.stamp_cycle_error(m.vertex, &circ_error, None);
21048 excluded[i] = true;
21049 stamped += 1;
21050 changed[i] = last_value[i] != circ_error;
21051 last_value[i] = circ_error.clone();
21052 } else {
21053 self.graph.update_vertex_value_ref(m.vertex, &value);
21054 self.mirror_vertex_value_to_overlay(m.vertex, &value);
21055 #[cfg(debug_assertions)]
21059 if let Some(cell) = m.cell {
21060 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21061 debug_assert!(
21062 self.read_delta_overlay_cell(
21063 &sheet_name,
21064 cell.coord.row() + 1,
21065 cell.coord.col() + 1
21066 )
21067 .is_none(),
21068 "user overlay must never shadow a formula SCC member ({sheet_name}!r{}c{})",
21069 cell.coord.row() + 1,
21070 cell.coord.col() + 1
21071 );
21072 }
21073 changed[i] = last_value[i] != value;
21074 last_value[i] = value;
21075 }
21076 }};
21077 }
21078
21079 let check_cancel = |flag: Option<&AtomicBool>| -> Result<(), ExcelError> {
21080 if let Some(flag) = flag
21081 && flag.load(Ordering::Relaxed)
21082 {
21083 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
21084 .with_message("Evaluation cancelled during SCC evaluation".to_string()));
21085 }
21086 Ok(())
21087 };
21088
21089 check_cancel(cancel_flag)?;
21091 let mut passes = 1usize;
21092 {
21093 let mut p = 0i64;
21094 for i in 0..n {
21095 if excluded[i] {
21096 continue;
21097 }
21098 run_member!(i);
21099 pos[i] = p;
21100 p += 1;
21101 }
21102 }
21103
21104 let policy = self.config.cycle.policy;
21120 let cap = n + 2;
21121 let mut witnessed_cycles = 0usize;
21122 let mut capped = false;
21123 let mut iterating = false;
21125 let mut converged = false;
21126 let mut exact_fixed_point = false;
21127 let mut prev_pass: Option<Vec<LiteralValue>> = None;
21131 let mut iter_max_delta = 0f64;
21134 let mut iter_nan_converged = 0usize;
21135 let mut settle_passes = 0usize;
21139 loop {
21140 let drained = collector.take_edges();
21143 for i in 0..n {
21144 if pos[i] >= 0 {
21145 out_edges[i].clear();
21146 }
21147 }
21148 for (from, to) in drained {
21149 debug_assert!(
21150 pos[from as usize] >= 0,
21151 "edge from a member that did not run"
21152 );
21153 out_edges[from as usize].push(to);
21154 }
21155 let mut edges: Vec<(u32, u32)> = Vec::new();
21156 for (i, outs) in out_edges.iter().enumerate() {
21157 if excluded[i] {
21158 continue;
21159 }
21160 for &t in outs {
21161 edges.push((i as u32, t));
21162 }
21163 }
21164 edges.sort_unstable();
21165 edges.dedup();
21166
21167 let analysis = analyze_live_graph(n, &edges);
21168
21169 if analysis.cycle_count > 0 {
21170 witnessed_cycles = witnessed_cycles.max(analysis.cycle_count);
21174 match policy {
21175 CyclePolicy::Error => {
21176 for i in 0..n {
21182 if analysis.in_cycle[i] && !excluded[i] {
21183 self.stamp_cycle_error(members[i].vertex, &circ_error, None);
21184 excluded[i] = true;
21185 last_value[i] = circ_error.clone();
21186 stamped += 1;
21187 }
21188 }
21189 check_cancel(cancel_flag)?;
21190 let order: Vec<usize> = analysis
21191 .topo
21192 .iter()
21193 .map(|&i| i as usize)
21194 .filter(|&i| !excluded[i])
21195 .collect();
21196 if !order.is_empty() {
21197 passes += 1;
21198 for i in order {
21199 run_member!(i);
21200 }
21201 }
21202 break;
21203 }
21204 CyclePolicy::Iterate {
21205 max_iterations,
21206 max_change,
21207 } => {
21208 iterating = true;
21210
21211 if let Some(prev) = &prev_pass {
21217 let mut round_max_delta = 0f64;
21218 let mut round_nan = 0usize;
21219 let mut all_converged = true;
21220 let mut round_exact = true;
21221 for i in 0..n {
21222 if excluded[i] {
21223 continue;
21227 }
21228 let out = crate::engine::convergence::values_converged(
21229 &prev[i],
21230 &last_value[i],
21231 max_change,
21232 self.config.date_system,
21233 );
21234 if out.nan_converged {
21235 round_nan += 1;
21236 }
21237 if let Some(d) = out.abs_delta {
21238 round_max_delta = round_max_delta.max(d);
21239 if d != 0.0 {
21240 round_exact = false;
21241 }
21242 }
21243 if !out.converged {
21244 all_converged = false;
21245 }
21246 }
21247 iter_max_delta = round_max_delta;
21250 iter_nan_converged = round_nan;
21251 if all_converged {
21252 exact_fixed_point = round_exact;
21253 converged = true;
21254 break;
21255 }
21256 }
21257
21258 if passes >= max_iterations as usize {
21270 capped = true;
21271 break;
21272 }
21273
21274 check_cancel(cancel_flag)?;
21275 prev_pass = Some(last_value.clone());
21283 for x in pos.iter_mut() {
21284 *x = -1;
21285 }
21286 changed.fill(false);
21287 passes += 1;
21288 let mut p = 0i64;
21289 for i in 0..n {
21290 if excluded[i] {
21291 continue;
21292 }
21293 run_member!(i);
21294 pos[i] = p;
21295 p += 1;
21296 }
21297 continue;
21298 }
21299 }
21300 }
21301
21302 let mut stale: Vec<usize> = Vec::new();
21305 for i in 0..n {
21306 if excluded[i] {
21307 continue;
21308 }
21309 let is_stale = out_edges[i].iter().any(|&t| {
21310 let t = t as usize;
21311 changed[t] && (pos[i] < 0 || (pos[t] >= 0 && pos[i] < pos[t]))
21312 });
21313 if is_stale {
21314 stale.push(i);
21315 }
21316 }
21317 if stale.is_empty() {
21318 break; }
21320 if 1 + settle_passes >= cap {
21321 capped = true;
21323 for (i, m) in members.iter().enumerate() {
21324 if !excluded[i] {
21325 self.stamp_cycle_error(m.vertex, &circ_error, None);
21326 excluded[i] = true;
21327 last_value[i] = circ_error.clone();
21328 stamped += 1;
21329 }
21330 }
21331 break;
21332 }
21333
21334 check_cancel(cancel_flag)?;
21335 prev_pass = None;
21342 let topo_pos = analysis.topo_positions();
21343 stale.sort_unstable_by_key(|&i| topo_pos[i]);
21344 for x in pos.iter_mut() {
21345 *x = -1;
21346 }
21347 changed.fill(false);
21348 passes += 1;
21349 settle_passes += 1;
21350 for (p, i) in stale.into_iter().enumerate() {
21351 run_member!(i);
21352 pos[i] = p as i64;
21353 }
21354 }
21355
21356 self.live_cancellation_after_work(SCC_CANCELLED)?;
21362
21363 if iterating && !converged && !capped {
21368 converged = true;
21369 }
21370
21371 collector.clear_current();
21374 if let Some(d) = delta
21375 && d.mode != DeltaMode::Off
21376 {
21377 for (i, m) in members.iter().enumerate() {
21378 if let Some(cell) = m.cell
21379 && last_value[i] != snapshot[i]
21380 {
21381 d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21382 }
21383 }
21384 }
21385
21386 if !self.retained_scc_members.is_empty() {
21404 for m in members.iter() {
21405 self.retained_scc_members.remove(&m.vertex);
21406 }
21407 }
21408 if iterating {
21409 let retain = converged
21410 && !capped
21411 && exact_fixed_point
21412 && iter_nan_converged == 0
21413 && members
21414 .iter()
21415 .all(|m| !self.graph.is_volatile(m.vertex) && !self.graph.is_dynamic(m.vertex));
21416 if retain {
21417 if self.retained_scc_members.is_empty() {
21418 self.retained_scc_config_fingerprint = self.retained_scc_config_fingerprint();
21419 self.retained_scc_function_epoch_seen =
21420 crate::function_registry::semantic_epoch();
21421 self.retained_scc_provider_revision_seen =
21422 self.resolver.planning_semantic_revision();
21423 }
21424 let scc_id = self.next_retained_scc_id;
21425 self.next_retained_scc_id = self.next_retained_scc_id.wrapping_add(1);
21426 for (i, m) in members.iter().enumerate() {
21427 self.retained_scc_members.insert(m.vertex, scc_id);
21428 if matches!(last_value[i], LiteralValue::Empty) {
21431 self.iterative_state_values.remove(&m.vertex);
21432 } else {
21433 self.iterative_state_values
21434 .insert(m.vertex, last_value[i].clone());
21435 }
21436 }
21437 } else {
21438 self.pending_iterative_redirty
21439 .extend(members.iter().map(|m| m.vertex));
21440 }
21441 } else if !self.iterative_state_values.is_empty() {
21442 for m in members.iter() {
21446 self.iterative_state_values.remove(&m.vertex);
21447 }
21448 }
21449
21450 {
21451 let t = &mut self.last_cycle_telemetry;
21452 t.static_sccs += 1;
21453 if witnessed_cycles == 0 && stamped == 0 && !capped {
21454 t.phantom_sccs += 1;
21455 }
21456 t.live_cycles_witnessed += witnessed_cycles;
21457 t.circ_cells_stamped += stamped;
21458 t.settle_passes_total += passes;
21459 t.max_passes_single_scc = t.max_passes_single_scc.max(passes);
21460 if iterating {
21461 t.iterated_sccs += 1;
21462 if converged {
21463 t.converged_sccs += 1;
21464 }
21465 t.max_abs_delta_at_stop = t.max_abs_delta_at_stop.max(iter_max_delta);
21466 t.nan_converged += iter_nan_converged;
21467 }
21468 if capped {
21469 t.capped_sccs += 1;
21470 }
21471 t.elapsed_ms += task_start.elapsed().as_millis();
21472 }
21473
21474 Ok(stamped)
21475 }
21476
21477 fn evaluate_vertex_recorded(
21485 &self,
21486 vertex_id: VertexId,
21487 ctx: &RecordingContext<'_, R>,
21488 collector: &LiveEdgeCollector,
21489 ) -> Result<LiteralValue, ExcelError> {
21490 if !self.graph.vertex_exists(vertex_id) {
21491 return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
21492 .with_message(format!("Vertex not found: {vertex_id:?}")));
21493 }
21494
21495 let kind = self.graph.get_vertex_kind(vertex_id);
21496 let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
21497
21498 match kind {
21499 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
21500 let Some(view) = self.graph.formula_view(vertex_id) else {
21501 return Ok(LiteralValue::Number(0.0)); };
21503 let sheet_name = self.graph.sheet_name(sheet_id);
21504 let cell_ref = self
21505 .graph
21506 .get_cell_ref(vertex_id)
21507 .expect("cell ref for vertex");
21508 let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref)
21509 .with_resolved_functions(&self.resolved_functions);
21510 interpreter
21511 .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
21512 .map(|cv| {
21513 let format = cv.format_id();
21514 self.record_derived_format(vertex_id, format);
21515 self.materialize_formula_result(vertex_id, cv)
21516 })
21517 .or_else(|error| self.fit_formula_error(vertex_id, error))
21518 }
21519 VertexKind::NamedScalar | VertexKind::NamedArray => {
21520 let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
21521 ExcelError::new(ExcelErrorKind::Name)
21522 .with_message("Named range metadata missing".to_string())
21523 })?;
21524
21525 match &named_range.definition {
21526 NamedDefinition::Formula { ast, .. } => {
21527 let _origin = self
21528 .operation_diagnostic_collector
21529 .as_ref()
21530 .map(|c| c.named_scope(vertex_id));
21531 let context_sheet = match named_range.scope {
21532 NameScope::Sheet(id) => id,
21533 NameScope::Workbook => sheet_id,
21534 };
21535 let sheet_name = self.graph.sheet_name(context_sheet);
21536 let cell_ref = self
21537 .graph
21538 .get_cell_ref(vertex_id)
21539 .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
21540 let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref)
21541 .with_resolved_functions(&self.resolved_functions);
21542 if kind == VertexKind::NamedScalar {
21543 interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
21544 } else {
21545 match interpreter.evaluate_ast(ast) {
21546 Ok(cv) => match cv.into_literal() {
21547 v @ LiteralValue::Array(_) => Ok(v),
21548 other => Ok(LiteralValue::Array(vec![vec![other]])),
21549 },
21550 Err(err) => Ok(LiteralValue::Error(err)),
21551 }
21552 }
21553 }
21554 NamedDefinition::Cell(cell_ref) => {
21555 collector.record_scalar(
21559 cell_ref.sheet_id,
21560 cell_ref.coord.row(),
21561 cell_ref.coord.col(),
21562 );
21563 self.evaluate_vertex_immutable(vertex_id)
21564 }
21565 NamedDefinition::Range(range_ref) => {
21566 if range_ref.start.sheet_id == range_ref.end.sheet_id {
21567 collector.record_rect(
21568 range_ref.start.sheet_id,
21569 range_ref.start.coord.row(),
21570 range_ref.start.coord.col(),
21571 range_ref.end.coord.row(),
21572 range_ref.end.coord.col(),
21573 );
21574 }
21575 self.evaluate_vertex_immutable(vertex_id)
21576 }
21577 NamedDefinition::Literal(_) => self.evaluate_vertex_immutable(vertex_id),
21578 }
21579 }
21580 _ => self.evaluate_vertex_immutable(vertex_id),
21581 }
21582 }
21583
21584 fn pending_spill_occupied(&self, anchor: CellRef, end_row: u32, end_col: u32) -> bool {
21586 let sheet = self.graph.sheet_name(anchor.sheet_id);
21587 let package = self
21588 .staged_formulas
21589 .get(sheet)
21590 .and_then(|staged| staged.deferred_package.as_ref());
21591 self.staged_formula_index.occupies_spill(
21592 sheet,
21593 (anchor.coord.row() + 1, anchor.coord.col() + 1),
21594 (end_row + 1, end_col + 1),
21595 |point| package.is_some_and(|package| package.suppressed.contains(&point)),
21596 )
21597 }
21598
21599 fn remember_pending_spill(
21600 &mut self,
21601 vertex: VertexId,
21602 anchor: CellRef,
21603 region: Region,
21604 ) -> Result<(), ExcelError> {
21605 self.cancellation_checkpoint("pending spill occupancy")?;
21606 self.resource_checkpoint(1)?;
21607 if let Some(entry) = self
21608 .blocked_pending_spills
21609 .iter_mut()
21610 .find(|entry| entry.0 == vertex)
21611 {
21612 *entry = (vertex, anchor, region);
21613 return Ok(());
21614 }
21615 if self.blocked_pending_spills.len() == self.blocked_pending_spills.capacity() {
21616 let additional = self.blocked_pending_spills.capacity().max(1);
21619 let bytes =
21620 (additional as u64)
21621 .saturating_mul(std::mem::size_of::<(VertexId, CellRef, Region)>() as u64);
21622 if let Some(ledger) = self.active_resource_ledger.as_mut() {
21623 ledger
21624 .reserve_retained(bytes)
21625 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
21626 self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
21627 }
21628 if self
21629 .blocked_pending_spills
21630 .try_reserve_exact(additional)
21631 .is_err()
21632 {
21633 if let Some(ledger) = self.active_resource_ledger.as_mut() {
21634 ledger
21635 .release_retained(bytes)
21636 .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
21637 self.source_cache_accounted -= bytes;
21638 }
21639 return Err(crate::engine::ResourceLedgerError::Exhausted(
21640 formualizer_common::ResourceExhaustionDetail {
21641 reason: formualizer_common::ResourceExhaustionReason::RetainedMemory,
21642 limit: u64::MAX,
21643 observed: bytes,
21644 request_id: None,
21645 },
21646 )
21647 .into_excel_error());
21648 }
21649 }
21650 self.blocked_pending_spills.push((vertex, anchor, region));
21651 Ok(())
21652 }
21653
21654 fn invalidate_pending_spills(&mut self, scope: StructuralScope) {
21658 if let StructuralScope::RemovedSheet(sheet) = scope {
21659 self.blocked_pending_spills
21660 .retain(|entry| entry.1.sheet_id != sheet);
21661 return;
21662 }
21663 for &(vertex, anchor, region) in &self.blocked_pending_spills {
21664 let affected = match scope {
21665 StructuralScope::Cell { sheet, row, col } => {
21666 region.intersects(&Region::point(sheet, row, col))
21667 }
21668 StructuralScope::Region(changed) => region.intersects(&changed),
21669 StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => {
21670 region.sheet_id() == sheet
21671 }
21672 StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => true,
21673 };
21674 if affected
21675 && self.graph.vertex_exists(vertex)
21676 && self.graph.get_cell_ref(vertex) == Some(anchor)
21677 && matches!(
21678 self.graph.get_vertex_kind(vertex),
21679 VertexKind::FormulaScalar | VertexKind::FormulaArray
21680 )
21681 {
21682 self.graph.mark_vertex_dirty(vertex);
21683 }
21684 }
21685 }
21686
21687 fn guard_pending_spill_commit(
21688 &mut self,
21689 anchor_vertex: VertexId,
21690 targets: &[CellRef],
21691 ) -> Result<(), ExcelError> {
21692 let Some(anchor) = self.graph.get_cell_ref(anchor_vertex) else {
21693 return Ok(());
21694 };
21695 let Some(last) = targets.last() else {
21696 return Ok(());
21697 };
21698 let occupied = self.pending_spill_occupied(anchor, last.coord.row(), last.coord.col())
21699 || self
21700 .graph
21701 .table_intersects_spill(anchor, last.coord.row(), last.coord.col());
21702 if (occupied
21703 || self
21704 .blocked_pending_spills
21705 .iter()
21706 .any(|entry| entry.0 == anchor_vertex))
21707 && let Err(error) = self.remember_pending_spill(
21708 anchor_vertex,
21709 anchor,
21710 Region::rect(
21711 anchor.sheet_id,
21712 anchor.coord.row(),
21713 last.coord.row(),
21714 anchor.coord.col(),
21715 last.coord.col(),
21716 ),
21717 )
21718 {
21719 self.spill_mgr.release_owner(anchor_vertex);
21720 return Err(error);
21721 }
21722 if occupied {
21723 self.spill_mgr.release_owner(anchor_vertex);
21724 return Err(ExcelError::new(ExcelErrorKind::Spill)
21725 .with_message("Spill blocked")
21726 .with_extra(formualizer_common::ExcelErrorExtra::Spill {
21727 expected_rows: last.coord.row() - anchor.coord.row() + 1,
21728 expected_cols: last.coord.col() - anchor.coord.col() + 1,
21729 }));
21730 }
21731 Ok(())
21732 }
21733
21734 fn commit_spill_and_mirror(
21736 &mut self,
21737 anchor_vertex: VertexId,
21738 targets: &[CellRef],
21739 rows: Vec<Vec<LiteralValue>>,
21740 delta: Option<&mut DeltaCollector>,
21741 ) -> Result<(), ExcelError> {
21742 self.guard_pending_spill_commit(anchor_vertex, targets)?;
21743 let prev_spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
21744
21745 if let Some(delta) = delta
21746 && delta.mode != DeltaMode::Off
21747 {
21748 let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
21749 targets.iter().copied().collect();
21750 let empty = LiteralValue::Empty;
21751
21752 for cell in prev_spill_cells.iter() {
21754 if target_set.contains(cell) {
21755 continue;
21756 }
21757 let sheet_name = self.graph.sheet_name(cell.sheet_id);
21758 let old = self
21759 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21760 .unwrap_or(LiteralValue::Empty);
21761 if old != empty {
21762 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21763 }
21764 }
21765
21766 if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
21768 let width = rows[0].len();
21769 for (idx, cell) in targets.iter().enumerate() {
21770 let r_off = idx / width;
21771 let c_off = idx % width;
21772 let new = rows
21773 .get(r_off)
21774 .and_then(|r| r.get(c_off))
21775 .cloned()
21776 .unwrap_or(LiteralValue::Empty);
21777 let sheet_name = self.graph.sheet_name(cell.sheet_id);
21778 let old = self
21779 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21780 .unwrap_or(LiteralValue::Empty);
21781 if old != new {
21782 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21783 }
21784 }
21785 } else {
21786 for cell in targets.iter() {
21788 let sheet_name = self.graph.sheet_name(cell.sheet_id);
21789 let old = self
21790 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21791 .unwrap_or(LiteralValue::Empty);
21792 if !matches!(old, LiteralValue::Empty) {
21793 delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21794 }
21795 }
21796 }
21797 }
21798
21799 let arrow_sheets = &self.arrow_sheets;
21802 self.spill_mgr.commit_array_with_value_probe(
21803 &mut self.graph,
21804 anchor_vertex,
21805 targets,
21806 rows.clone(),
21807 |g, cell| {
21808 let sheet_name = g.sheet_name(cell.sheet_id);
21809 let asheet = arrow_sheets.sheet(sheet_name)?;
21810 let r0 = cell.coord.row() as usize;
21811 let c0 = cell.coord.col() as usize;
21812 let v = asheet.get_cell_value(r0, c0);
21813 if matches!(v, LiteralValue::Empty) {
21814 None
21815 } else {
21816 Some(v)
21817 }
21818 },
21819 )?;
21820
21821 self.freshness_note_spill_change(&prev_spill_cells, targets);
21822 self.blocked_pending_spills
21823 .retain(|entry| entry.0 != anchor_vertex);
21824 self.spill_blocker_waiters.forget(anchor_vertex);
21825 if let Some(scope) = Self::structural_scope_from_cells(&prev_spill_cells) {
21826 self.record_structural_change(scope);
21827 }
21828 if let Some(scope) = Self::structural_scope_from_cells(targets) {
21829 self.record_structural_change(scope);
21830 }
21831
21832 if self.config.arrow_storage_enabled
21833 && self.config.delta_overlay_enabled
21834 && self.config.write_formula_overlay_enabled
21835 {
21836 if !prev_spill_cells.is_empty() {
21837 let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
21838 targets.iter().copied().collect();
21839 let empty = LiteralValue::Empty;
21840 for cell in prev_spill_cells.iter() {
21841 if !target_set.contains(cell) {
21842 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21843 self.mirror_value_to_computed_overlay(
21844 &sheet_name,
21845 cell.coord.row() + 1,
21846 cell.coord.col() + 1,
21847 &empty,
21848 );
21849 }
21850 }
21851 }
21852
21853 for (idx, cell) in targets.iter().enumerate() {
21854 if rows.is_empty() || rows[0].is_empty() {
21855 break;
21856 }
21857 let width = rows[0].len();
21858 let r_off = idx / width;
21859 let c_off = idx % width;
21860 let v = rows[r_off][c_off].clone();
21861 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21862 self.mirror_value_to_computed_overlay(
21863 &sheet_name,
21864 cell.coord.row() + 1,
21865 cell.coord.col() + 1,
21866 &v,
21867 );
21868 }
21869 }
21870 Ok(())
21871 }
21872}
21873
21874#[cfg(test)]
21875#[path = "tests/authority_schedule_execution.rs"]
21876mod authority_schedule_execution;
21877
21878#[cfg(test)]
21879#[path = "tests/scalar_commit.rs"]
21880mod scalar_commit_tests;
21881
21882#[cfg(test)]
21883#[path = "tests/observed_effect_identity.rs"]
21884mod observed_effect_identity;
21885
21886#[cfg(test)]
21887#[path = "tests/pending_spill.rs"]
21888mod pending_spill_tests;
21889
21890#[cfg(test)]
21891#[path = "tests/spill_batch_abort_192.rs"]
21892mod spill_batch_abort_192;
21893
21894#[cfg(test)]
21895#[path = "tests/observed_future_spill_329.rs"]
21896mod observed_future_spill_329;
21897
21898use crate::engine::effects::Effect;
21903
21904type PlannedEffects = smallvec::SmallVec<[Effect; 2]>;
21906use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
21907
21908impl<R> Engine<R>
21909where
21910 R: EvaluationContext,
21911{
21912 pub(crate) fn plan_vertex_effects(
21919 &mut self,
21920 vertex_id: VertexId,
21921 computed_value: LiteralValue,
21922 ) -> Result<PlannedEffects, ExcelError> {
21923 if self.freshness_armed() {
21926 if self.freshness_drop_stale(vertex_id) {
21927 return Ok(PlannedEffects::new());
21928 }
21929 let mut effects = self.plan_vertex_effects_unrecorded(vertex_id, computed_value)?;
21930 self.freshness_bind_planned_result(vertex_id, &mut effects);
21931 self.freshness_mark_committed(vertex_id);
21932 return Ok(effects);
21933 }
21934 self.plan_vertex_effects_unrecorded(vertex_id, computed_value)
21935 }
21936
21937 fn plan_vertex_effects_unrecorded(
21938 &mut self,
21939 vertex_id: VertexId,
21940 computed_value: LiteralValue,
21941 ) -> Result<PlannedEffects, ExcelError> {
21942 let kind = self.graph.get_vertex_kind(vertex_id);
21943 let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
21944
21945 if !is_formula {
21949 if let Some(cell) = self.graph.get_cell_ref(vertex_id)
21950 && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
21951 && owner != vertex_id
21952 {
21953 return Ok(PlannedEffects::new());
21954 }
21955 return Ok(smallvec::smallvec![Effect::WriteCell {
21957 vertex_id,
21958 value: computed_value,
21959 }]);
21960 }
21961
21962 match computed_value {
21963 LiteralValue::Array(rows) => self.plan_array_effects(vertex_id, rows),
21964 other => self.plan_scalar_effects(vertex_id, other),
21965 }
21966 }
21967
21968 fn plan_scalar_effects(
21970 &mut self,
21971 vertex_id: VertexId,
21972 value: LiteralValue,
21973 ) -> Result<PlannedEffects, ExcelError> {
21974 if let LiteralValue::Error(error) = &value
21977 && error.kind == ExcelErrorKind::Spill
21978 && error.message() == Some("SpillTooLarge")
21979 && let formualizer_common::ExcelErrorExtra::Spill {
21980 expected_rows,
21981 expected_cols,
21982 } = error.extra()
21983 && u64::from(*expected_rows).saturating_mul(u64::from(*expected_cols))
21984 > u64::from(self.config.spill.max_spill_cells)
21985 {
21986 self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
21987 return self.plan_spill_error_effects(
21988 vertex_id,
21989 "SpillTooLarge",
21990 *expected_rows,
21991 *expected_cols,
21992 );
21993 }
21994 if !matches!(&value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Spill) {
21995 self.blocked_pending_spills
21996 .retain(|entry| entry.0 != vertex_id);
21997 self.spill_blocker_waiters.forget(vertex_id);
21998 }
21999 let has_spill = self
22000 .graph
22001 .spill_cells_for_anchor(vertex_id)
22002 .is_some_and(|c| !c.is_empty());
22003
22004 let mut effects = PlannedEffects::new();
22005 if has_spill {
22006 effects.push(Effect::SpillClear {
22007 anchor_vertex: vertex_id,
22008 });
22009 }
22010 effects.push(Effect::WriteCell { vertex_id, value });
22011 Ok(effects)
22012 }
22013
22014 fn plan_array_effects(
22016 &mut self,
22017 vertex_id: VertexId,
22018 rows: Vec<Vec<LiteralValue>>,
22019 ) -> Result<PlannedEffects, ExcelError> {
22020 self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
22022
22023 let anchor = self
22024 .graph
22025 .get_cell_ref(vertex_id)
22026 .expect("cell ref for vertex");
22027 let sheet_id = anchor.sheet_id;
22028 let h = rows.len() as u32;
22029 let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
22030
22031 let spill_cells = (h as u64).saturating_mul(w as u64);
22033 if spill_cells > self.config.spill.max_spill_cells as u64 {
22034 return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
22035 }
22036
22037 const PACKED_MAX_ROW: u32 = 1_048_575;
22039 const PACKED_MAX_COL: u32 = 16_383;
22040 let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
22041 let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
22042 if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
22043 return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
22044 }
22045
22046 if h != 0 && w != 0 && self.graph.table_intersects_spill(anchor, end_row, end_col) {
22047 return self.plan_spill_error_effects(vertex_id, "Spill blocked by table", h, w);
22048 }
22049 let mut targets = Vec::new();
22050 for r in 0..h {
22051 for c in 0..w {
22052 targets.push(self.graph.make_cell_ref_internal(
22053 sheet_id,
22054 anchor.coord.row() + r,
22055 anchor.coord.col() + c,
22056 ));
22057 }
22058 }
22059
22060 if h != 0 && w != 0 {
22061 let occupied = self.pending_spill_occupied(anchor, end_row, end_col);
22062 if occupied
22063 || self
22064 .blocked_pending_spills
22065 .iter()
22066 .any(|entry| entry.0 == vertex_id)
22067 {
22068 self.spill_mgr.release_owner(vertex_id);
22069 self.remember_pending_spill(
22070 vertex_id,
22071 anchor,
22072 Region::rect(
22073 sheet_id,
22074 anchor.coord.row(),
22075 end_row,
22076 anchor.coord.col(),
22077 end_col,
22078 ),
22079 )?;
22080 }
22081 if occupied {
22082 return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
22083 }
22084 }
22085
22086 match self.spill_mgr.reserve(
22088 vertex_id,
22089 anchor,
22090 SpillShape { rows: h, cols: w },
22091 SpillMeta {
22092 epoch: self.recalc_epoch,
22093 config: self.config.spill,
22094 },
22095 ) {
22096 Ok(()) => {
22097 let mut yield_effects = PlannedEffects::new();
22099 if let Err((e, blocker)) = self
22100 .graph
22101 .plan_spill_region_with_blocker(vertex_id, &targets)
22102 {
22103 let yielding = if e.message() == Some("BlockedBySpill") {
22104 self.graph.spills_yielding_to(vertex_id, &targets)
22105 } else {
22106 None
22107 };
22108 if let Some(yielding) = yielding {
22109 for &other in &yielding {
22113 let (rows, cols) = self
22114 .graph
22115 .spill_extent_for_anchor(other)
22116 .map(|(first, last)| {
22117 (
22118 last.coord.row() - first.coord.row() + 1,
22119 last.coord.col() - first.coord.col() + 1,
22120 )
22121 })
22122 .unwrap_or((0, 0));
22123 yield_effects.push(Effect::SpillClear {
22124 anchor_vertex: other,
22125 });
22126 yield_effects.push(Effect::WriteCell {
22127 vertex_id: other,
22128 value: Self::spill_error_value("Spill blocked", rows, cols),
22129 });
22130 }
22131 self.graph.mark_dirty_many(&yielding);
22132 } else {
22133 if matches!(e.message(), Some("BlockedByFormula" | "BlockedBySpill")) {
22137 self.spill_blocker_waiters.wait(vertex_id, blocker);
22138 }
22139 return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
22140 }
22141 }
22142
22143 if !self.graph.value_cache_enabled() {
22147 let sheet_name = self.graph.sheet_name(sheet_id);
22148 if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
22149 for cell in targets.iter() {
22150 if *cell == anchor {
22152 continue;
22153 }
22154 if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
22156 continue;
22157 }
22158 if let Some(vid) = self.graph.get_vertex_id_for_address(cell)
22160 && vid != vertex_id
22161 {
22162 match self.graph.get_vertex_kind(vid) {
22163 VertexKind::FormulaScalar | VertexKind::FormulaArray => {
22164 continue;
22165 }
22166 _ => {}
22167 }
22168 }
22169
22170 let v = asheet.get_cell_value(
22171 cell.coord.row() as usize,
22172 cell.coord.col() as usize,
22173 );
22174 if !matches!(v, LiteralValue::Empty) {
22175 self.spill_blocker_waiters.wait(vertex_id, *cell);
22178 return self.plan_spill_error_effects(
22179 vertex_id,
22180 "BlockedByValue",
22181 h,
22182 w,
22183 );
22184 }
22185 }
22186 }
22187 }
22188
22189 let top_left = rows
22190 .first()
22191 .and_then(|r| r.first())
22192 .cloned()
22193 .unwrap_or(LiteralValue::Empty);
22194
22195 let mut effects = yield_effects;
22196 let has_prev = self
22198 .graph
22199 .spill_cells_for_anchor(vertex_id)
22200 .is_some_and(|c| !c.is_empty());
22201 if has_prev {
22202 effects.push(Effect::SpillClear {
22203 anchor_vertex: vertex_id,
22204 });
22205 }
22206 effects.push(Effect::SpillCommit {
22207 anchor_vertex: vertex_id,
22208 anchor_cell: anchor,
22209 target_cells: targets,
22210 values: rows,
22211 });
22212 effects.push(Effect::WriteCell {
22213 vertex_id,
22214 value: top_left,
22215 });
22216 Ok(effects)
22217 }
22218 Err(e) => {
22219 let msg = e.message().unwrap_or("Spill blocked");
22220 self.plan_spill_error_effects(vertex_id, msg, h, w)
22221 }
22222 }
22223 }
22224
22225 fn spill_error_value(message: &str, expected_rows: u32, expected_cols: u32) -> LiteralValue {
22227 LiteralValue::Error(
22228 ExcelError::new(ExcelErrorKind::Spill)
22229 .with_message(message)
22230 .with_extra(formualizer_common::ExcelErrorExtra::Spill {
22231 expected_rows,
22232 expected_cols,
22233 }),
22234 )
22235 }
22236
22237 fn plan_spill_error_effects(
22239 &mut self,
22240 vertex_id: VertexId,
22241 message: &str,
22242 expected_rows: u32,
22243 expected_cols: u32,
22244 ) -> Result<PlannedEffects, ExcelError> {
22245 self.spill_mgr.release_owner(vertex_id);
22246 let spill_val = Self::spill_error_value(message, expected_rows, expected_cols);
22247
22248 let effects = smallvec::smallvec![
22249 Effect::SpillClear {
22250 anchor_vertex: vertex_id,
22251 },
22252 Effect::WriteCell {
22253 vertex_id,
22254 value: spill_val,
22255 },
22256 ];
22257 Ok(effects)
22258 }
22259
22260 pub(crate) fn apply_effect(
22262 &mut self,
22263 effect: &Effect,
22264 delta: Option<&mut DeltaCollector>,
22265 log: Option<&mut ChangeLog>,
22266 ) -> Result<(), ExcelError> {
22267 self.apply_effect_with_computed_writes(effect, delta, log, None)
22268 }
22269
22270 fn apply_effect_with_computed_writes(
22271 &mut self,
22272 effect: &Effect,
22273 delta: Option<&mut DeltaCollector>,
22274 log: Option<&mut ChangeLog>,
22275 mut computed_writes: Option<&mut ComputedWriteBuffer>,
22276 ) -> Result<(), ExcelError> {
22277 match effect {
22278 Effect::WriteCell { vertex_id, value } => {
22279 self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
22280 }
22281 Effect::WriteRecordedCell { result } => {
22282 let vertex = result.vertex();
22283 let disposition = self.apply_write_cell(
22284 vertex,
22285 result.value(),
22286 delta,
22287 computed_writes.as_deref_mut(),
22288 )?;
22289 match disposition {
22290 OverlayWriteDisposition::Installed => {
22291 self.freshness_record_installed(result, true)
22292 }
22293 OverlayWriteDisposition::Skipped => self.freshness_record_installed(
22294 result,
22295 self.graph.get_cell_ref(vertex).is_none(),
22296 ),
22297 OverlayWriteDisposition::Buffered => {
22298 let buffer = computed_writes.expect("buffered write has its buffer");
22299 let cell = self.graph.get_cell_ref(vertex).expect("buffered grid cell");
22300 let seq = buffer.writes.last().expect("staged write").seq();
22301 buffer.read_claims.insert(
22302 (cell.sheet_id, cell.coord.row(), cell.coord.col()),
22303 BufferedReadClaim {
22304 seq,
22305 result: result.clone(),
22306 winning: false,
22307 },
22308 );
22309 }
22310 }
22311 }
22312 Effect::SpillClear { anchor_vertex } => {
22313 self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
22314 }
22315 Effect::SpillCommit {
22316 anchor_vertex,
22317 anchor_cell: _,
22318 target_cells,
22319 values,
22320 } => {
22321 self.apply_spill_commit(
22322 *anchor_vertex,
22323 target_cells,
22324 values.clone(),
22325 delta,
22326 log,
22327 computed_writes,
22328 )?;
22329 }
22330 }
22331 Ok(())
22332 }
22333
22334 fn apply_write_cell(
22337 &mut self,
22338 vertex_id: VertexId,
22339 value: &LiteralValue,
22340 delta: Option<&mut DeltaCollector>,
22341 mut computed_writes: Option<&mut ComputedWriteBuffer>,
22342 ) -> Result<OverlayWriteDisposition, ExcelError> {
22343 if let Some(d) = delta
22344 && d.mode != DeltaMode::Off
22345 {
22346 if let Some(buffer) = computed_writes.as_deref_mut() {
22347 self.flush_computed_write_buffer(buffer)?;
22348 }
22349 if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
22350 let sheet_name = self.graph.sheet_name(cell.sheet_id);
22351 let old = self
22352 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22353 .unwrap_or(LiteralValue::Empty);
22354 if old != *value {
22355 d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
22356 }
22357 }
22358 }
22359 self.graph.update_vertex_value_ref(vertex_id, value);
22360 self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)
22361 }
22362
22363 fn apply_spill_clear(
22365 &mut self,
22366 anchor_vertex: VertexId,
22367 delta: Option<&mut DeltaCollector>,
22368 log: Option<&mut ChangeLog>,
22369 computed_writes: Option<&mut ComputedWriteBuffer>,
22370 ) -> Result<(), ExcelError> {
22371 if let Some(buffer) = computed_writes {
22372 self.flush_computed_write_buffer(buffer)?;
22373 }
22374
22375 let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
22376 if spill_cells.is_empty() {
22377 return Ok(());
22378 }
22379 self.graph
22380 .authority_host_mut()
22381 .invalidate_observed_value(anchor_vertex);
22382
22383 let snapshot = if log.is_some() {
22385 self.snapshot_spill_for_anchor(anchor_vertex)
22386 } else {
22387 None
22388 };
22389
22390 if let Some(d) = delta
22392 && d.mode != DeltaMode::Off
22393 {
22394 let empty = LiteralValue::Empty;
22395 for cell in spill_cells.iter() {
22396 let sheet_name = self.graph.sheet_name(cell.sheet_id);
22397 let old = self
22398 .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22399 .unwrap_or(LiteralValue::Empty);
22400 if old != empty {
22401 d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
22402 }
22403 }
22404 }
22405
22406 self.graph.clear_spill_region(anchor_vertex);
22407 self.freshness_note_spill_change(&spill_cells, &[]);
22408 if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
22409 self.record_structural_change(scope);
22410 }
22411
22412 if self.config.arrow_storage_enabled
22414 && self.config.delta_overlay_enabled
22415 && self.config.write_formula_overlay_enabled
22416 {
22417 let empty = LiteralValue::Empty;
22418 for cell in spill_cells.iter() {
22419 let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
22420 self.mirror_value_to_computed_overlay(
22421 &sheet_name,
22422 cell.coord.row() + 1,
22423 cell.coord.col() + 1,
22424 &empty,
22425 );
22426 }
22427 }
22428
22429 if let Some(log) = log
22431 && let Some(old) = snapshot
22432 {
22433 log.record(ChangeEvent::SpillCleared {
22434 anchor: anchor_vertex,
22435 old,
22436 });
22437 }
22438 Ok(())
22439 }
22440
22441 fn apply_spill_commit(
22443 &mut self,
22444 anchor_vertex: VertexId,
22445 target_cells: &[CellRef],
22446 values: Vec<Vec<LiteralValue>>,
22447 delta: Option<&mut DeltaCollector>,
22448 log: Option<&mut ChangeLog>,
22449 computed_writes: Option<&mut ComputedWriteBuffer>,
22450 ) -> Result<(), ExcelError> {
22451 self.guard_pending_spill_commit(anchor_vertex, target_cells)?;
22452 if let Some(buffer) = computed_writes {
22453 self.flush_computed_write_buffer(buffer)?;
22454 }
22455
22456 let old_snapshot = if log.is_some() {
22458 self.snapshot_spill_for_anchor(anchor_vertex)
22459 } else {
22460 None
22461 };
22462
22463 self.commit_spill_and_mirror(anchor_vertex, target_cells, values.clone(), delta)?;
22465
22466 if let Some(log) = log {
22468 log.record(ChangeEvent::SpillCommitted {
22469 anchor: anchor_vertex,
22470 old: old_snapshot,
22471 new: SpillSnapshot {
22472 target_cells: target_cells.to_vec(),
22473 values,
22474 },
22475 });
22476 }
22477 Ok(())
22478 }
22479
22480 fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
22485 let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
22486 if cells.is_empty() {
22487 return None;
22488 }
22489
22490 let max = self.config.spill.max_spill_cells as usize;
22491 let mut cells = cells;
22492 if cells.len() > max {
22493 cells.truncate(max);
22494 }
22495
22496 let first = *cells.first().expect("non-empty spill cells");
22497 let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
22498 let row0 = first.coord.row();
22499 let col0 = first.coord.col();
22500
22501 let mut max_row = row0;
22502 let mut max_col = col0;
22503 let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
22504 for cell in &cells {
22505 max_row = max_row.max(cell.coord.row());
22506 max_col = max_col.max(cell.coord.col());
22507 let v = self
22508 .read_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22509 .unwrap_or(LiteralValue::Empty);
22510 by_coord.insert((cell.coord.row(), cell.coord.col()), v);
22511 }
22512
22513 let rows = (max_row - row0 + 1) as usize;
22514 let cols = (max_col - col0 + 1) as usize;
22515 let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
22516 for r in 0..rows {
22517 let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
22518 for c in 0..cols {
22519 row.push(
22520 by_coord
22521 .get(&(row0 + r as u32, col0 + c as u32))
22522 .cloned()
22523 .unwrap_or(LiteralValue::Empty),
22524 );
22525 }
22526 values.push(row);
22527 }
22528
22529 if self.graph.spill_may_be_intruded(anchor) {
22530 cells.retain(|cell| {
22531 self.graph.spill_registry_anchor_for_cell(*cell) == Some(anchor)
22532 && !self.graph.is_foreign_formula_cell(cell, anchor)
22533 });
22534 }
22535 Some(SpillSnapshot {
22536 target_cells: cells,
22537 values,
22538 })
22539 }
22540
22541 fn flush_before_range_dependent_vertex(
22542 &mut self,
22543 vertex_id: VertexId,
22544 computed_writes: &mut ComputedWriteBuffer,
22545 ) -> Result<(), ExcelError> {
22546 if self.graph.reads_compressed_range(vertex_id) {
22547 self.flush_computed_write_buffer(computed_writes)?;
22548 }
22549 Ok(())
22550 }
22551
22552 fn plan_vertex_effects_with_computed_flush(
22553 &mut self,
22554 vertex_id: VertexId,
22555 computed_value: LiteralValue,
22556 computed_writes: &mut ComputedWriteBuffer,
22557 ) -> Result<PlannedEffects, ExcelError> {
22558 if matches!(&computed_value, LiteralValue::Array(_)) {
22559 self.flush_computed_write_buffer(computed_writes)?;
22560 }
22561 self.plan_vertex_effects(vertex_id, computed_value)
22562 }
22563
22564 fn evaluate_small_layer_direct_effects(
22567 &mut self,
22568 layer: &super::scheduler::Layer,
22569 delta: Option<&mut DeltaCollector>,
22570 log: Option<&mut ChangeLog>,
22571 cancel_flag: Option<&AtomicBool>,
22572 cancel_check_every: usize,
22573 cancel_message: &'static str,
22574 ) -> Result<usize, ExcelError> {
22575 let cancel = cancel_flag.map(|flag| (flag, cancel_check_every, cancel_message));
22576 self.evaluate_layer_units(layer, delta, log, cancel, false)
22577 }
22578
22579 fn evaluate_layer_units(
22586 &mut self,
22587 layer: &super::scheduler::Layer,
22588 delta: Option<&mut DeltaCollector>,
22589 log: Option<&mut ChangeLog>,
22590 cancel: Option<(&AtomicBool, usize, &'static str)>,
22591 buffered: bool,
22592 ) -> Result<usize, ExcelError> {
22593 self.evaluate_layer_units_until(layer, delta, log, cancel, buffered, None)
22594 }
22595
22596 fn evaluate_layer_units_until(
22600 &mut self,
22601 layer: &super::scheduler::Layer,
22602 mut delta: Option<&mut DeltaCollector>,
22603 mut log: Option<&mut ChangeLog>,
22604 cancel: Option<(&AtomicBool, usize, &'static str)>,
22605 buffered: bool,
22606 stop_at: Option<crate::instant::FzInstant>,
22607 ) -> Result<usize, ExcelError> {
22608 let mut chain_values = None;
22611 if layer.sequential && !layer.runs.is_empty() {
22612 chain_values = match layer.runs.as_slice() {
22613 [run] if run.start == 0 && run.len as usize == layer.vertices.len() => {
22614 self.try_chain_lift(*run, &layer.vertices)
22615 }
22616 _ => None,
22617 };
22618 if chain_values.is_none() {
22619 let cells = super::scheduler::Layer {
22620 vertices: layer.vertices.clone(),
22621 runs: Vec::new(),
22622 sequential: true,
22623 };
22624 return self.evaluate_layer_units_until(&cells, delta, log, cancel, false, stop_at);
22625 }
22626 }
22627 let chained = chain_values.is_some();
22628 let buffered = buffered || chained;
22630 let track_unflushed = buffered
22637 && self.freshness_armed()
22638 && layer.vertices.iter().any(|&v| self.graph.is_dynamic(v));
22639 let mut committed_unit: &[VertexId] = &[];
22640 let mut computed_writes = ComputedWriteBuffer::default();
22641 let mut next_check = 0usize;
22642 let mut done = 0usize;
22643 for unit in layer_units(layer) {
22644 if done > 0
22645 && let Some(stop_at) = stop_at
22646 && crate::instant::FzInstant::now() >= stop_at
22647 {
22648 self.flush_computed_write_buffer(&mut computed_writes)?;
22649 if track_unflushed {
22650 self.freshness_flushed();
22651 }
22652 return Ok(done);
22653 }
22654 if let Some((flag, every, message)) = cancel
22655 && every > 0
22656 && done >= next_check
22657 {
22658 next_check = (done / every + 1) * every;
22659 if flag.load(Ordering::Relaxed) {
22660 if buffered {
22661 self.flush_computed_write_buffer(&mut computed_writes)?;
22662 }
22663 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
22664 .with_message(message.to_string()));
22665 }
22666 }
22667 if buffered && self.unit_reads_compressed_range(layer, unit) {
22668 self.flush_computed_write_buffer(&mut computed_writes)?;
22669 }
22670 if track_unflushed {
22674 if computed_writes.is_empty() {
22675 self.freshness_flushed();
22676 } else {
22677 self.freshness_note_unflushed(committed_unit);
22678 }
22679 committed_unit = unit_members(layer, unit);
22680 }
22681 let values = match (chain_values.take(), unit) {
22682 (Some(chain), LayerUnit::Run(run)) => {
22683 if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
22684 self.flush_computed_write_buffer(&mut computed_writes)?;
22685 return Err(e);
22686 }
22687 let members =
22688 &layer.vertices[run.start as usize..(run.start + run.len) as usize];
22689 let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
22690 match self.commit_run_numbers(
22691 run,
22692 members,
22693 &chain,
22694 delta_active,
22695 Some(&mut computed_writes),
22696 ) {
22697 Ok(true) => {
22698 done += chain.len();
22699 continue;
22700 }
22701 Ok(false) => {}
22702 Err(e) => {
22703 self.flush_computed_write_buffer(&mut computed_writes)?;
22704 return Err(e);
22705 }
22706 }
22707 members
22708 .iter()
22709 .copied()
22710 .zip(chain.into_iter().map(LiteralValue::Number))
22711 .collect()
22712 }
22713 (_, unit) => self.evaluate_unit_immutable(layer, unit),
22714 };
22715 if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
22718 self.flush_computed_write_buffer(&mut computed_writes)?;
22719 return Err(e);
22720 }
22721 done += values.len();
22722 if let LayerUnit::Run(run) = unit {
22723 let members = &layer.vertices[run.start as usize..(run.start + run.len) as usize];
22724 let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
22725 let committed = self.commit_run_scalars(
22726 run,
22727 members,
22728 &values,
22729 delta_active,
22730 buffered.then_some(&mut computed_writes),
22731 );
22732 match committed {
22733 Ok(true) => continue,
22734 Ok(false) => {}
22735 Err(e) => {
22736 self.flush_computed_write_buffer(&mut computed_writes)?;
22737 return Err(e);
22738 }
22739 }
22740 }
22741 let guarded = self.freshness_begin_batch_commit(&values);
22744 for (vertex_id, value) in values {
22745 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22746 let effects = if buffered {
22747 self.plan_vertex_effects_with_computed_flush(
22748 vertex_id,
22749 value,
22750 &mut computed_writes,
22751 )
22752 } else {
22753 self.plan_vertex_effects(vertex_id, value)
22754 };
22755 let effects = match effects {
22756 Ok(effects) => effects,
22757 Err(e) => {
22758 self.flush_computed_write_buffer(&mut computed_writes)?;
22759 return Err(e);
22760 }
22761 };
22762 for effect in &effects {
22763 if let Err(e) = self.apply_effect_with_computed_writes(
22764 effect,
22765 delta.as_deref_mut(),
22766 log.as_deref_mut(),
22767 buffered.then_some(&mut computed_writes),
22768 ) {
22769 self.flush_computed_write_buffer(&mut computed_writes)?;
22770 return Err(e);
22771 }
22772 }
22773 self.freshness_keep_redirtied(redirtied, vertex_id);
22774 }
22775 }
22776 self.flush_computed_write_buffer(&mut computed_writes)?;
22777 if track_unflushed {
22778 self.freshness_flushed();
22779 }
22780 #[cfg(debug_assertions)]
22783 if chained {
22784 for &v in &layer.vertices {
22785 let cell = self.graph.get_cell_ref(v);
22786 let (sheet, row, col) = cell
22787 .map(|c| {
22788 (
22789 self.graph.sheet_name(c.sheet_id).to_string(),
22790 c.coord.row() + 1,
22791 c.coord.col() + 1,
22792 )
22793 })
22794 .expect("chain member cell");
22795 let written = self.read_cell_value(&sheet, row, col);
22796 let oracle = self
22797 .evaluate_vertex_immutable(v)
22798 .unwrap_or_else(LiteralValue::Error);
22799 assert!(
22800 written
22801 .as_ref()
22802 .is_some_and(|w| same_value_bits(w, &oracle)),
22803 "chain member {sheet}!R{row}C{col}: {written:?} vs per-cell {oracle:?}"
22804 );
22805 }
22806 }
22807 #[cfg(not(debug_assertions))]
22808 let _ = chained;
22809 Ok(layer.vertices.len())
22810 }
22811
22812 fn evaluate_layer_sequential_effects(
22814 &mut self,
22815 layer: &super::scheduler::Layer,
22816 ) -> Result<usize, ExcelError> {
22817 let buffered = buffer_layer_writes(layer);
22818 self.evaluate_layer_units(layer, None, None, None, buffered)
22819 }
22820
22821 fn evaluate_layer_sequential_with_delta_effects(
22823 &mut self,
22824 layer: &super::scheduler::Layer,
22825 delta: &mut DeltaCollector,
22826 ) -> Result<usize, ExcelError> {
22827 let buffered = buffer_layer_writes(layer);
22828 self.evaluate_layer_units(layer, Some(delta), None, None, buffered)
22829 }
22830
22831 fn evaluate_layer_sequential_cancellable_effects(
22833 &mut self,
22834 layer: &super::scheduler::Layer,
22835 cancel_flag: &AtomicBool,
22836 ) -> Result<usize, ExcelError> {
22837 let buffered = buffer_layer_writes(layer);
22838 let cancel = (cancel_flag, 256, "Evaluation cancelled within layer");
22839 self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
22840 }
22841
22842 fn evaluate_layer_sequential_cancellable_demand_driven_effects(
22844 &mut self,
22845 layer: &super::scheduler::Layer,
22846 cancel_flag: &AtomicBool,
22847 ) -> Result<usize, ExcelError> {
22848 let buffered = buffer_layer_writes(layer);
22849 let cancel = (
22850 cancel_flag,
22851 128,
22852 "Demand-driven evaluation cancelled within layer",
22853 );
22854 self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
22855 }
22856
22857 fn evaluate_layer_parallel_effects(
22859 &mut self,
22860 layer: &super::scheduler::Layer,
22861 min_chunk: u32,
22862 ) -> Result<usize, ExcelError> {
22863 let thread_pool = self.thread_pool.as_ref().unwrap().clone();
22864
22865 let phases = self.parallel_phases(layer);
22866 let mut applied = 0usize;
22867
22868 for (units, group) in &phases {
22869 let group = &group[..];
22870 if group.is_empty() {
22871 continue;
22872 }
22873 let mut computed_writes = ComputedWriteBuffer::default();
22874
22875 let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
22876 thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, min_chunk));
22877
22878 let results = results.and_then(|results| {
22881 self.live_cancellation_after_work(GROUP_CANCELLED)
22882 .map(|()| results)
22883 });
22884 self.freshness_gate_group(group);
22887 match results {
22888 Ok(vertex_results) => {
22889 let (vertex_results, committed) = self.commit_parallel_runs(
22890 layer,
22891 units,
22892 vertex_results,
22893 false,
22894 &mut computed_writes,
22895 )?;
22896 applied = applied.saturating_add(committed);
22897 let guarded = self.freshness_begin_batch_commit(&vertex_results);
22902 let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
22903 let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
22904 for (vertex_id, result) in vertex_results {
22905 if matches!(result, LiteralValue::Array(_)) {
22906 arrays.push((vertex_id, result));
22907 } else {
22908 others.push((vertex_id, result));
22909 }
22910 }
22911 for (vertex_id, result) in arrays {
22912 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22913 let effects = match self.plan_vertex_effects_with_computed_flush(
22914 vertex_id,
22915 result,
22916 &mut computed_writes,
22917 ) {
22918 Ok(effects) => effects,
22919 Err(e) => {
22920 self.flush_computed_write_buffer(&mut computed_writes)?;
22921 return Err(e);
22922 }
22923 };
22924 for effect in &effects {
22925 if let Err(e) = self.apply_effect_with_computed_writes(
22926 effect,
22927 None,
22928 None,
22929 Some(&mut computed_writes),
22930 ) {
22931 self.flush_computed_write_buffer(&mut computed_writes)?;
22932 return Err(e);
22933 }
22934 }
22935 self.freshness_keep_redirtied(redirtied, vertex_id);
22936 applied = applied.saturating_add(1);
22937 }
22938 self.flush_computed_write_buffer(&mut computed_writes)?;
22940 for (vertex_id, result) in others {
22941 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22942 let effects = match self.plan_vertex_effects_with_computed_flush(
22943 vertex_id,
22944 result,
22945 &mut computed_writes,
22946 ) {
22947 Ok(effects) => effects,
22948 Err(e) => {
22949 self.flush_computed_write_buffer(&mut computed_writes)?;
22950 return Err(e);
22951 }
22952 };
22953 for effect in &effects {
22954 if let Err(e) = self.apply_effect_with_computed_writes(
22955 effect,
22956 None,
22957 None,
22958 Some(&mut computed_writes),
22959 ) {
22960 self.flush_computed_write_buffer(&mut computed_writes)?;
22961 return Err(e);
22962 }
22963 }
22964 self.freshness_keep_redirtied(redirtied, vertex_id);
22965 applied = applied.saturating_add(1);
22966 }
22967 self.flush_computed_write_buffer(&mut computed_writes)?;
22969 }
22970 Err(e) => {
22971 self.flush_computed_write_buffer(&mut computed_writes)?;
22972 return Err(e);
22973 }
22974 }
22975 }
22976
22977 Ok(applied)
22978 }
22979
22980 fn evaluate_layer_parallel_with_delta_effects(
22982 &mut self,
22983 layer: &super::scheduler::Layer,
22984 delta: &mut DeltaCollector,
22985 ) -> Result<usize, ExcelError> {
22986 let thread_pool = self.thread_pool.as_ref().unwrap().clone();
22987
22988 let phases = self.parallel_phases(layer);
22989 let mut applied = 0usize;
22990
22991 for (units, group) in &phases {
22992 let group = &group[..];
22993 if group.is_empty() {
22994 continue;
22995 }
22996 let mut computed_writes = ComputedWriteBuffer::default();
22997 let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
22998 thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, 8));
22999
23000 let results = results.and_then(|results| {
23003 self.live_cancellation_after_work(GROUP_CANCELLED)
23004 .map(|()| results)
23005 });
23006 self.freshness_gate_group(group);
23009 match results {
23010 Ok(vertex_results) => {
23011 let (vertex_results, committed) = self.commit_parallel_runs(
23012 layer,
23013 units,
23014 vertex_results,
23015 delta.mode != DeltaMode::Off,
23016 &mut computed_writes,
23017 )?;
23018 applied = applied.saturating_add(committed);
23019 let guarded = self.freshness_begin_batch_commit(&vertex_results);
23022 let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
23023 let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
23024 for (vertex_id, result) in vertex_results {
23025 if matches!(result, LiteralValue::Array(_)) {
23026 arrays.push((vertex_id, result));
23027 } else {
23028 others.push((vertex_id, result));
23029 }
23030 }
23031 for (vertex_id, result) in arrays {
23032 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23033 let effects = match self.plan_vertex_effects_with_computed_flush(
23034 vertex_id,
23035 result,
23036 &mut computed_writes,
23037 ) {
23038 Ok(effects) => effects,
23039 Err(e) => {
23040 self.flush_computed_write_buffer(&mut computed_writes)?;
23041 return Err(e);
23042 }
23043 };
23044 for effect in &effects {
23045 if let Err(e) = self.apply_effect_with_computed_writes(
23046 effect,
23047 Some(delta),
23048 None,
23049 Some(&mut computed_writes),
23050 ) {
23051 self.flush_computed_write_buffer(&mut computed_writes)?;
23052 return Err(e);
23053 }
23054 }
23055 self.freshness_keep_redirtied(redirtied, vertex_id);
23056 applied = applied.saturating_add(1);
23057 }
23058 self.flush_computed_write_buffer(&mut computed_writes)?;
23059 for (vertex_id, result) in others {
23060 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23061 let effects = match self.plan_vertex_effects_with_computed_flush(
23062 vertex_id,
23063 result,
23064 &mut computed_writes,
23065 ) {
23066 Ok(effects) => effects,
23067 Err(e) => {
23068 self.flush_computed_write_buffer(&mut computed_writes)?;
23069 return Err(e);
23070 }
23071 };
23072 for effect in &effects {
23073 if let Err(e) = self.apply_effect_with_computed_writes(
23074 effect,
23075 Some(delta),
23076 None,
23077 Some(&mut computed_writes),
23078 ) {
23079 self.flush_computed_write_buffer(&mut computed_writes)?;
23080 return Err(e);
23081 }
23082 }
23083 self.freshness_keep_redirtied(redirtied, vertex_id);
23084 applied = applied.saturating_add(1);
23085 }
23086 self.flush_computed_write_buffer(&mut computed_writes)?;
23087 }
23088 Err(e) => {
23089 self.flush_computed_write_buffer(&mut computed_writes)?;
23090 return Err(e);
23091 }
23092 }
23093 }
23094
23095 Ok(applied)
23096 }
23097
23098 fn evaluate_layer_parallel_cancellable_effects(
23100 &mut self,
23101 layer: &super::scheduler::Layer,
23102 cancel_flag: &AtomicBool,
23103 ) -> Result<usize, ExcelError> {
23104 let thread_pool = self.thread_pool.as_ref().unwrap().clone();
23105
23106 if cancel_flag.load(Ordering::Relaxed) {
23107 return Err(ExcelError::new(ExcelErrorKind::Cancelled)
23108 .with_message("Parallel evaluation cancelled before starting".to_string()));
23109 }
23110
23111 let phases = self.parallel_phases(layer);
23112 let mut applied = 0usize;
23113
23114 for (units, group) in &phases {
23115 let group = &group[..];
23116 if group.is_empty() {
23117 continue;
23118 }
23119 let mut computed_writes = ComputedWriteBuffer::default();
23120
23121 let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> = thread_pool
23122 .install(|| self.evaluate_units_parallel(layer, units, Some(cancel_flag), 8));
23123
23124 let results = results.and_then(|results| {
23127 self.live_cancellation_after_work(GROUP_CANCELLED)
23128 .map(|()| results)
23129 });
23130 self.freshness_gate_group(group);
23133 match results {
23134 Ok(vertex_results) => {
23135 let (vertex_results, committed) = self.commit_parallel_runs(
23136 layer,
23137 units,
23138 vertex_results,
23139 false,
23140 &mut computed_writes,
23141 )?;
23142 applied = applied.saturating_add(committed);
23143 let guarded = self.freshness_begin_batch_commit(&vertex_results);
23146 let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
23147 let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
23148 for (vertex_id, result) in vertex_results {
23149 if matches!(result, LiteralValue::Array(_)) {
23150 arrays.push((vertex_id, result));
23151 } else {
23152 others.push((vertex_id, result));
23153 }
23154 }
23155 for (vertex_id, result) in arrays {
23156 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23157 let effects = match self.plan_vertex_effects_with_computed_flush(
23158 vertex_id,
23159 result,
23160 &mut computed_writes,
23161 ) {
23162 Ok(effects) => effects,
23163 Err(e) => {
23164 self.flush_computed_write_buffer(&mut computed_writes)?;
23165 return Err(e);
23166 }
23167 };
23168 for effect in &effects {
23169 if let Err(e) = self.apply_effect_with_computed_writes(
23170 effect,
23171 None,
23172 None,
23173 Some(&mut computed_writes),
23174 ) {
23175 self.flush_computed_write_buffer(&mut computed_writes)?;
23176 return Err(e);
23177 }
23178 }
23179 self.freshness_keep_redirtied(redirtied, vertex_id);
23180 applied = applied.saturating_add(1);
23181 }
23182 self.flush_computed_write_buffer(&mut computed_writes)?;
23183 for (vertex_id, result) in others {
23184 let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23185 let effects = match self.plan_vertex_effects_with_computed_flush(
23186 vertex_id,
23187 result,
23188 &mut computed_writes,
23189 ) {
23190 Ok(effects) => effects,
23191 Err(e) => {
23192 self.flush_computed_write_buffer(&mut computed_writes)?;
23193 return Err(e);
23194 }
23195 };
23196 for effect in &effects {
23197 if let Err(e) = self.apply_effect_with_computed_writes(
23198 effect,
23199 None,
23200 None,
23201 Some(&mut computed_writes),
23202 ) {
23203 self.flush_computed_write_buffer(&mut computed_writes)?;
23204 return Err(e);
23205 }
23206 }
23207 self.freshness_keep_redirtied(redirtied, vertex_id);
23208 applied = applied.saturating_add(1);
23209 }
23210 self.flush_computed_write_buffer(&mut computed_writes)?;
23211 }
23212 Err(e) => {
23213 self.flush_computed_write_buffer(&mut computed_writes)?;
23214 return Err(e);
23215 }
23216 }
23217 }
23218
23219 Ok(applied)
23220 }
23221
23222 pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
23229 self.ensure_not_poisoned()?;
23230 self.observe_evaluation_resource_request(EvaluationRequestKind::FullLogged, |engine| {
23231 engine.evaluate_all_logged_unobserved(log)
23232 })
23233 }
23234
23235 fn evaluate_all_logged_unobserved(
23236 &mut self,
23237 log: &mut ChangeLog,
23238 ) -> Result<EvalResult, ExcelError> {
23239 self.observe_function_semantic_epoch()?;
23240 let _source_cache = self.source_cache_session();
23241 self.validate_deterministic_mode()?;
23242 if self.config.defer_graph_building {
23243 self.build_graph_all()?;
23244 }
23245 self.require_unified_authority()?;
23246 self.begin_evaluation_request();
23247 self.reset_virtual_dep_telemetry_if_disabled();
23248 let start = crate::instant::FzInstant::now();
23249 let mut computed_vertices = 0;
23250 let mut cycle_errors = 0;
23251
23252 let mut replan_iterations = 0;
23253 const MAX_REPLAN: usize = 5;
23254 let mut telemetry = self
23255 .config
23256 .enable_virtual_dep_telemetry
23257 .then(|| self.start_virtual_dep_telemetry());
23258
23259 log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
23260
23261 let result = (|| -> Result<EvalResult, ExcelError> {
23262 loop {
23263 let to_evaluate = self.graph.get_evaluation_vertices();
23264 if to_evaluate.is_empty() {
23265 if let Some(t) = telemetry.as_mut()
23266 && t.bailout_reason.is_none()
23267 {
23268 t.bailout_reason = Some("no_work");
23269 }
23270 break;
23271 }
23272
23273 let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
23274 if let Some(t) = telemetry.as_mut() {
23275 Self::accumulate_schedule_meta(t, &meta);
23276 }
23277
23278 self.begin_pass(&schedule);
23281 for (unit_index, &unit) in schedule.units.iter().enumerate() {
23282 match unit {
23283 ScheduleUnit::Cycle(i) => {
23284 if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0
23295 {
23296 cycle_errors += 1;
23297 }
23298 }
23299 ScheduleUnit::Layer(i) => {
23300 computed_vertices +=
23301 self.evaluate_layer_logged(schedule.unit_layer(i), log)?;
23302 }
23303 }
23304 if self.stop_after_unit(&schedule, unit_index) {
23305 break;
23306 }
23307 }
23308
23309 let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
23310 if let Some(t) = telemetry.as_mut() {
23311 t.changed_vdeps_total += changed_vertices.len();
23312 }
23313 self.resource_checkpoint(0)?;
23314 if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
23315 if let Some(t) = telemetry.as_mut() {
23316 t.bailout_reason = Some("converged");
23317 }
23318 break;
23319 }
23320 if replan_iterations >= MAX_REPLAN {
23321 if let Some(mut t) = telemetry.take() {
23322 t.bailout_reason = Some("max_replan");
23323 t.replan_iterations = replan_iterations;
23324 self.last_virtual_dep_telemetry = t;
23325 }
23326 return Err(
23327 self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
23328 );
23329 }
23330 replan_iterations += 1;
23331 }
23332
23333 if let Some(mut t) = telemetry {
23334 t.replan_iterations = replan_iterations;
23335 self.last_virtual_dep_telemetry = t;
23336 }
23337
23338 self.redirty_for_next_recalc();
23339 self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
23340
23341 Ok(EvalResult {
23342 computed_vertices,
23343 cycle_errors,
23344 elapsed: start.elapsed(),
23345 })
23346 })();
23347 log.end_compound();
23348 result
23349 }
23350
23351 fn evaluate_layer_logged(
23353 &mut self,
23354 layer: &super::scheduler::Layer,
23355 log: &mut ChangeLog,
23356 ) -> Result<usize, ExcelError> {
23357 self.resource_checkpoint(layer.vertices.len() as u64)?;
23358 self.evaluate_layer_units(layer, None, Some(log), None, true)
23359 }
23360}
23361
23362impl<R: EvaluationContext> Engine<R> {
23363 pub fn is_poisoned(&self) -> bool {
23374 self.poisoned.load(std::sync::atomic::Ordering::Acquire)
23375 }
23376
23377 pub(crate) fn ensure_not_poisoned(&self) -> Result<(), ExcelError> {
23378 if self.is_poisoned() {
23379 Err(ExcelError::new(ExcelErrorKind::Value)
23380 .with_message(super::poison::POISON_MESSAGE)
23381 .with_extra(formualizer_common::ExcelErrorExtra::EnginePoisoned))
23382 } else {
23383 Ok(())
23384 }
23385 }
23386
23387 pub(crate) fn assert_not_poisoned(&self) {
23388 assert!(!self.is_poisoned(), "{}", super::poison::POISON_MESSAGE);
23389 }
23390}