1pub mod arrow_ingest;
6pub(crate) mod convergence;
7pub mod effects;
8pub mod eval;
9pub mod eval_delta;
10pub mod formula_ingest;
11pub mod graph;
12pub mod ingest;
13pub mod ingest_builder;
14pub(crate) mod ingest_pipeline;
15pub mod journal;
16pub mod live_edges;
17pub mod live_graph;
18pub mod lookup_index_cache;
19pub mod plan;
20pub mod range_view;
21pub mod row_visibility;
22pub mod scheduler;
23pub mod spill;
24pub mod vertex;
25pub mod virtual_deps;
26
27pub mod csr_edges;
29pub mod debug_views;
30pub mod delta_edges;
31pub mod interval_tree;
32pub mod named_range;
33pub mod sheet_index;
34pub mod sheet_registry;
35pub mod topo;
36pub mod vertex_store;
37
38pub mod arena;
40
41pub mod tuning;
43
44#[cfg(test)]
45mod tests;
46
47pub use arena::AstNodeId;
48pub use eval::{
49 CycleTelemetry, Engine, EngineAction, EngineBaselineStats, EvalResult, RecalcPlan,
50 VirtualDepTelemetry,
51};
52pub use eval_delta::{DeltaMode, EvalDelta};
53pub use formula_ingest::{FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport};
54pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
55pub use graph::snapshot::VertexSnapshot;
57pub use graph::{
58 ChangeEvent, DependencyGraph, DependencyRef, GraphBaselineStats, OperationSummary, StripeKey,
59 StripeType, block_index,
60};
61pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
62pub use scheduler::{Layer, Schedule, ScheduleUnit, Scheduler};
63pub use vertex::{VertexId, VertexKind};
64
65pub use graph::editor::{
66 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
67 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
68};
69
70pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
71
72#[doc(hidden)]
73pub mod fp8_parity_test_support {
74 use super::{Engine, EvalConfig};
75 use crate::engine::arena::CanonicalLabels;
76 use crate::formula_plane::dependency_summary::summarize_canonical_template;
77 use crate::formula_plane::producer::SpanReadSummary;
78 use crate::formula_plane::runtime::{PlacementDomain, ResultRegion};
79 use crate::formula_plane::template_canonical::{
80 CanonicalRejectReason, CanonicalTemplateFlag, canonicalize_template,
81 };
82 use crate::reference::{CellRef, Coord};
83 use crate::traits::EvaluationContext;
84 use formualizer_common::{ExcelError, LiteralValue};
85 use formualizer_parse::parser::{ASTNode, parse};
86 use std::sync::Arc;
87
88 #[derive(Clone, Debug)]
89 pub struct Fp8ParityObservation {
90 pub formula: String,
91 pub placement: CellRef,
92 pub old_payload: String,
93 pub new_hash: u64,
94 }
95
96 pub fn default_config() -> EvalConfig {
97 EvalConfig::default()
98 }
99
100 pub fn parse_formula(formula: &str) -> ASTNode {
101 parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"))
102 }
103
104 pub fn cell(sheet_id: u16, row: u32, col: u32) -> CellRef {
105 CellRef::new(sheet_id, Coord::from_excel(row, col, true, true))
106 }
107
108 pub fn assert_case<R: EvaluationContext>(
109 engine: &mut Engine<R>,
110 formula: &str,
111 placement: CellRef,
112 ) -> Fp8ParityObservation {
113 let parsed = parse_formula(formula);
114 assert_case_ast(engine, formula, parsed, placement)
115 }
116
117 pub fn assert_case_ast<R: EvaluationContext>(
118 engine: &mut Engine<R>,
119 formula: &str,
120 parsed: ASTNode,
121 placement: CellRef,
122 ) -> Fp8ParityObservation {
123 let mut old_ast = parsed.clone();
124 let old_rewrite = engine
125 .graph
126 .rewrite_structured_references_for_cell(&mut old_ast, placement);
127 let old = old_rewrite.and_then(|_| old_path(engine, &old_ast, placement));
128
129 let new = {
130 let mut pipeline = engine.ingest_pipeline();
131 pipeline.ingest_formula(
132 crate::engine::ingest_pipeline::FormulaAstInput::Tree(parsed),
133 placement,
134 Some(Arc::<str>::from(formula)),
135 )
136 };
137
138 match (old, new) {
139 (Ok(old), Ok(new)) => {
140 let new_direct = sorted_cells(new.dep_plan.direct_cell_deps.clone());
141 assert_eq!(
142 old.direct_cells, new_direct,
143 "direct deps differ for {formula} at {placement:?}\nold={:?}\nnew={:?}",
144 old.direct_cells, new_direct
145 );
146 assert_eq!(
147 old.range_deps, new.dep_plan.range_deps,
148 "range deps differ for {formula} at {placement:?}"
149 );
150 assert_eq!(
151 old.unresolved_names, new.dep_plan.named_refs,
152 "unresolved names differ for {formula} at {placement:?}"
153 );
154 assert_eq!(
155 old.volatile, new.dep_plan.volatile,
156 "volatile flag differs for {formula} at {placement:?}"
157 );
158 assert_eq!(
159 old.dynamic, new.dep_plan.dynamic,
160 "dynamic flag differs for {formula} at {placement:?}"
161 );
162 let mut expected_labels = canonical_labels_from_old(&old.labels);
163 if old.dynamic {
164 expected_labels.flags |= CanonicalLabels::FLAG_DYNAMIC;
165 }
166 assert_eq!(
167 expected_labels.flags, new.labels.flags,
168 "canonical label flags differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
169 old.labels.flags, new.labels.flags
170 );
171 assert_eq!(
172 expected_labels.rejects, new.labels.rejects,
173 "canonical label rejects differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
174 old.labels.reject_reasons, new.labels.rejects
175 );
176 let named_resolution_superset = old.summary_rejected_only_for_named_reference
182 && old.read_summary_debug.is_none();
183 if !named_resolution_superset {
184 assert_eq!(
185 old.read_summary_debug,
186 new.read_summary.as_ref().map(|s| format!("{s:?}")),
187 "read summary differs for {formula} at {placement:?}"
188 );
189 }
190 assert_eq!(new.formula_text.as_deref(), Some(formula));
191 assert_eq!(new.placement, placement);
192 Fp8ParityObservation {
193 formula: formula.to_string(),
194 placement,
195 old_payload: old.payload,
196 new_hash: new.canonical_hash,
197 }
198 }
199 (Err(old), Err(new)) => {
200 assert_eq!(
201 old.kind.to_string(),
202 new.kind.to_string(),
203 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
204 );
205 Fp8ParityObservation {
206 formula: formula.to_string(),
207 placement,
208 old_payload: format!("ERR:{:?}", old.kind),
209 new_hash: 0,
210 }
211 }
212 (Ok(_), Err(new)) => panic!(
213 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
214 ),
215 (Err(old), Ok(_)) => panic!(
216 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
217 ),
218 }
219 }
220
221 #[derive(Debug)]
222 struct OldOutput {
223 payload: String,
224 labels: crate::formula_plane::template_canonical::CanonicalTemplateLabels,
225 direct_cells: Vec<CellRef>,
226 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
227 unresolved_names: Vec<String>,
228 volatile: bool,
229 dynamic: bool,
230 read_summary_debug: Option<String>,
231 summary_rejected_only_for_named_reference: bool,
232 }
233
234 fn old_path<R: EvaluationContext>(
235 engine: &mut Engine<R>,
236 ast: &ASTNode,
237 placement: CellRef,
238 ) -> Result<OldOutput, ExcelError> {
239 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
240 .graph
241 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
242 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
243 let dynamic = engine.graph.is_ast_dynamic(ast);
244 let template =
245 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
246 let summary = summarize_canonical_template(&template);
247 let scalar_domain = PlacementDomain::row_run(
248 placement.sheet_id,
249 placement.coord.row(),
250 placement.coord.row(),
251 placement.coord.col(),
252 );
253 let result_region = ResultRegion::scalar_cells(scalar_domain);
254 let read_summary = SpanReadSummary::from_formula_summary(
255 placement.sheet_id,
256 &result_region,
257 &summary,
258 engine.graph.sheet_reg(),
259 )
260 .ok();
261 let summary_rejected_only_for_named_reference = !summary.reject_reasons.is_empty()
262 && summary.reject_reasons.iter().all(|reason| {
263 matches!(
264 reason,
265 crate::formula_plane::dependency_summary::DependencyRejectReason
266 ::NamedRangeUnsupported { .. }
267 )
268 });
269 Ok(OldOutput {
270 payload: template.key.payload().to_string(),
271 labels: template.labels,
272 direct_cells: sorted_cells(placeholders),
273 range_deps: ranges,
274 unresolved_names,
275 volatile,
276 dynamic,
277 read_summary_debug: read_summary.as_ref().map(|s| format!("{s:?}")),
278 summary_rejected_only_for_named_reference,
279 })
280 }
281
282 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
283 cells.sort();
284 cells.dedup();
285 cells
286 }
287
288 fn canonical_labels_from_old(
289 old: &crate::formula_plane::template_canonical::CanonicalTemplateLabels,
290 ) -> CanonicalLabels {
291 let mut labels = CanonicalLabels::default();
292 for flag in &old.flags {
293 labels.flags |= match flag {
294 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
295 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
296 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
297 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
298 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
299 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
300 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
301 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
302 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
303 };
304 }
305 for reason in &old.reject_reasons {
306 labels.flags |= match reason {
307 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
308 CanonicalLabels::FLAG_DYNAMIC
309 }
310 CanonicalRejectReason::ParserVolatileFlag
311 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
312 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
313 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
314 }
315 CanonicalRejectReason::ArrayOrSpillFunction { .. }
316 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
317 CanonicalRejectReason::StructuredReference { .. }
318 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
319 CanonicalLabels::FLAG_CONTAINS_TABLE
320 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
321 }
322 CanonicalRejectReason::OpenRangeReference { .. }
323 | CanonicalRejectReason::WholeAxisReference { .. } => {
324 CanonicalLabels::FLAG_CONTAINS_RANGE
325 }
326 _ => 0,
327 };
328 labels.rejects |= match reason {
329 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
330 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
331 }
332 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
333 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
334 }
335 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
336 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
337 }
338 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
339 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
340 }
341 CanonicalRejectReason::ParserVolatileFlag => {
342 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
343 }
344 CanonicalRejectReason::VolatileFunction { .. } => {
345 CanonicalLabels::REJECT_VOLATILE_FUNCTION
346 }
347 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
348 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
349 }
350 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
351 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
352 }
353 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
354 CanonicalRejectReason::SpillReference { .. } => {
355 CanonicalLabels::REJECT_SPILL_REFERENCE
356 }
357 CanonicalRejectReason::SpillResultRegionOperator => {
358 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
359 }
360 CanonicalRejectReason::ImplicitIntersectionOperator => {
361 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
362 }
363 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
364 CanonicalRejectReason::StructuredReference { .. } => {
365 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
366 }
367 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
368 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
369 }
370 CanonicalRejectReason::ThreeDReference { .. } => {
371 CanonicalLabels::REJECT_THREE_D_REFERENCE
372 }
373 CanonicalRejectReason::ExternalReference { .. } => {
374 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
375 }
376 CanonicalRejectReason::OpenRangeReference { .. } => {
377 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
378 }
379 CanonicalRejectReason::WholeAxisReference { .. } => {
380 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
381 }
382 CanonicalRejectReason::UnsupportedReference { .. } => {
383 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
384 }
385 };
386 }
387 labels
388 }
389
390 pub fn literal_number(value: f64) -> LiteralValue {
391 LiteralValue::Number(value)
392 }
393}
394
395use crate::timezone::TimeZoneSpec;
398use crate::traits::EvaluationContext;
399use crate::traits::VolatileLevel;
400use chrono::{DateTime, Utc};
401use formualizer_common::error::{ExcelError, ExcelErrorKind};
402use std::collections::HashMap;
403
404impl<R: EvaluationContext> Engine<R> {
405 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
406 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
407 }
408
409 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
410 self.graph.store_ast(ast)
411 }
412}
413
414pub trait CalcObserver: Send + Sync {
416 fn on_eval_start(&self, vertex_id: VertexId);
417 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
418 fn on_cycle_detected(&self, cycle: &[VertexId]);
419 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
420}
421
422impl CalcObserver for () {
424 fn on_eval_start(&self, _vertex_id: VertexId) {}
425 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
426 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
427 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
434pub enum DeterministicMode {
435 Disabled {
437 timezone: TimeZoneSpec,
439 },
440 Enabled {
442 timestamp_utc: DateTime<Utc>,
444 timezone: TimeZoneSpec,
446 },
447}
448
449impl Default for DeterministicMode {
450 fn default() -> Self {
451 Self::Disabled {
452 timezone: TimeZoneSpec::default(),
453 }
454 }
455}
456
457impl DeterministicMode {
458 pub fn is_enabled(&self) -> bool {
459 matches!(self, DeterministicMode::Enabled { .. })
460 }
461
462 pub fn timezone(&self) -> &TimeZoneSpec {
463 match self {
464 DeterministicMode::Disabled { timezone } => timezone,
465 DeterministicMode::Enabled { timezone, .. } => timezone,
466 }
467 }
468
469 pub fn validate(&self) -> Result<(), ExcelError> {
470 if let DeterministicMode::Enabled { timezone, .. } = self {
471 timezone
472 .validate_for_determinism()
473 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
474 }
475 Ok(())
476 }
477
478 pub fn build_clock(
479 &self,
480 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
481 self.validate()?;
482 Ok(match self {
483 #[cfg(feature = "system-clock")]
484 DeterministicMode::Disabled { timezone } => {
485 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
486 }
487 #[cfg(not(feature = "system-clock"))]
488 DeterministicMode::Disabled { timezone: _ } => {
489 std::sync::Arc::new(crate::timezone::FixedClock::new(
494 chrono::DateTime::UNIX_EPOCH,
495 crate::timezone::TimeZoneSpec::Utc,
496 ))
497 }
498 DeterministicMode::Enabled {
499 timestamp_utc,
500 timezone,
501 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
502 *timestamp_utc,
503 timezone.clone(),
504 )),
505 })
506 }
507}
508
509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum FormulaParsePolicy {
512 Strict,
514 CoerceToError,
516 KeepCachedValue,
518 AsText,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
524pub struct FormulaParseDiagnostic {
525 pub sheet: String,
526 pub row: u32,
527 pub col: u32,
528 pub formula: String,
529 pub message: String,
530 pub policy: FormulaParsePolicy,
531}
532
533#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
534pub enum FormulaPlaneMode {
535 #[default]
538 Off,
539 Shadow,
540 AuthoritativeExperimental,
543}
544
545#[derive(Debug, Clone, PartialEq, Eq)]
547pub struct WorkbookLoadLimits {
548 pub max_sheet_rows: u32,
550 pub max_sheet_cols: u32,
552 pub max_sheet_logical_cells: u64,
554 pub sparse_sheet_cell_threshold: u64,
556 pub max_sparse_cell_ratio: u64,
558}
559
560impl Default for WorkbookLoadLimits {
561 fn default() -> Self {
562 Self {
563 max_sheet_rows: 1_048_576,
564 max_sheet_cols: 16_384,
565 max_sheet_logical_cells: 128_000_000,
566 sparse_sheet_cell_threshold: 250_000,
567 max_sparse_cell_ratio: 1_024,
568 }
569 }
570}
571
572#[derive(Debug, Clone)]
574pub struct EvalConfig {
575 pub enable_parallel: bool,
576 pub max_threads: Option<usize>,
577 pub max_vertices: Option<usize>,
579 pub max_eval_time: Option<std::time::Duration>,
580 pub max_memory_mb: Option<usize>,
581
582 pub default_sheet_name: String,
584
585 pub case_sensitive_names: bool,
589
590 pub case_sensitive_tables: bool,
594
595 pub workbook_seed: u64,
597
598 pub volatile_level: VolatileLevel,
600
601 pub deterministic_mode: DeterministicMode,
603
604 pub range_expansion_limit: usize,
607
608 pub max_open_ended_rows: u32,
612
613 pub max_open_ended_cols: u32,
617
618 pub stripe_height: u32,
620 pub stripe_width: u32,
622 pub enable_block_stripes: bool,
624
625 pub spill: SpillConfig,
627
628 pub cycle: CycleConfig,
632
633 pub use_dynamic_topo: bool,
635 pub pk_visit_budget: usize,
637 pub pk_compaction_interval_ops: u64,
639 pub max_layer_width: Option<usize>,
641 pub pk_reject_cycle_edges: bool,
644 pub sheet_index_mode: SheetIndexMode,
646
647 pub warmup: tuning::WarmupConfig,
649
650 pub arrow_storage_enabled: bool,
652 pub delta_overlay_enabled: bool,
654
655 pub write_formula_overlay_enabled: bool,
658
659 pub max_overlay_memory_bytes: Option<usize>,
664
665 pub date_system: DateSystem,
667
668 pub formula_parse_policy: FormulaParsePolicy,
670
671 pub defer_graph_building: bool,
674
675 pub enable_virtual_dep_telemetry: bool,
679
680 pub formula_plane_mode: FormulaPlaneMode,
685
686 pub lookup_index_cache_max_bytes: usize,
688}
689
690impl Default for EvalConfig {
691 fn default() -> Self {
692 Self {
693 enable_parallel: true,
694 max_threads: None,
695 max_vertices: None,
696 max_eval_time: None,
697 max_memory_mb: None,
698
699 default_sheet_name: format!("Sheet{}", 1),
700
701 case_sensitive_names: false,
703 case_sensitive_tables: false,
704
705 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
707
708 volatile_level: VolatileLevel::Always,
710
711 deterministic_mode: DeterministicMode::default(),
712
713 range_expansion_limit: 64,
715 max_open_ended_rows: 1_048_576,
718 max_open_ended_cols: 16_384,
719 stripe_height: 256,
720 stripe_width: 256,
721 enable_block_stripes: false,
722 spill: SpillConfig::default(),
723 cycle: CycleConfig::default(),
724
725 use_dynamic_topo: false, pk_visit_budget: 50_000,
728 pk_compaction_interval_ops: 100_000,
729 max_layer_width: None,
730 pk_reject_cycle_edges: false,
731 sheet_index_mode: SheetIndexMode::Eager,
732 warmup: tuning::WarmupConfig::default(),
733 arrow_storage_enabled: true,
734 delta_overlay_enabled: true,
735 write_formula_overlay_enabled: true,
736 max_overlay_memory_bytes: None,
737 date_system: DateSystem::Excel1900,
738 formula_parse_policy: FormulaParsePolicy::Strict,
739 defer_graph_building: false,
740 enable_virtual_dep_telemetry: false,
741 formula_plane_mode: FormulaPlaneMode::Off,
742 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
743 }
744 }
745}
746
747impl EvalConfig {
748 #[inline]
749 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
750 self.range_expansion_limit = limit;
751 self
752 }
753
754 #[inline]
755 pub fn with_parallel(mut self, enable: bool) -> Self {
756 self.enable_parallel = enable;
757 self
758 }
759
760 #[inline]
761 pub fn with_block_stripes(mut self, enable: bool) -> Self {
762 self.enable_block_stripes = enable;
763 self
764 }
765
766 #[inline]
767 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
768 self.case_sensitive_names = enable;
769 self
770 }
771
772 #[inline]
773 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
774 self.case_sensitive_tables = enable;
775 self
776 }
777
778 #[inline]
779 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
780 self.arrow_storage_enabled = enable;
781 self
782 }
783
784 #[inline]
785 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
786 self.delta_overlay_enabled = enable;
787 self
788 }
789
790 #[inline]
791 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
792 self.write_formula_overlay_enabled = enable;
793 self
794 }
795
796 #[inline]
797 pub fn with_date_system(mut self, system: DateSystem) -> Self {
798 self.date_system = system;
799 self
800 }
801
802 #[inline]
803 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
804 self.formula_parse_policy = policy;
805 self
806 }
807
808 #[inline]
809 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
810 self.enable_virtual_dep_telemetry = enable;
811 self
812 }
813
814 #[inline]
815 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
816 self.formula_plane_mode = mode;
817 self
818 }
819
820 #[inline]
829 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
830 if let Err(msg) = cycle.validate() {
831 panic!("invalid CycleConfig: {msg}");
832 }
833 self.cycle = cycle;
834 self
835 }
836}
837
838#[derive(Debug, Clone, Copy, PartialEq, Default)]
843pub struct CycleConfig {
844 pub detection: CycleDetection,
845 pub policy: CyclePolicy,
846}
847
848impl CycleConfig {
849 pub fn iterate_excel_defaults() -> Self {
852 Self {
853 detection: CycleDetection::Runtime,
854 policy: CyclePolicy::iterate_excel_defaults(),
855 }
856 }
857
858 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
860 Self {
861 detection: CycleDetection::Runtime,
862 policy: CyclePolicy::Iterate {
863 max_iterations,
864 max_change,
865 },
866 }
867 }
868
869 pub fn validate(&self) -> Result<(), String> {
873 if let CyclePolicy::Iterate {
874 max_iterations,
875 max_change,
876 } = self.policy
877 {
878 if self.detection == CycleDetection::Static {
879 return Err(
880 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
881 );
882 }
883 if max_iterations == 0 {
884 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
885 }
886 if !max_change.is_finite() || max_change < 0.0 {
887 return Err(format!(
888 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
889 ));
890 }
891 }
892 Ok(())
893 }
894
895 #[inline]
900 pub(crate) fn allows_self_dependency(&self) -> bool {
901 self.detection == CycleDetection::Runtime
902 && matches!(self.policy, CyclePolicy::Iterate { .. })
903 }
904}
905
906#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
908pub enum CycleDetection {
909 #[default]
912 Static,
913 Runtime,
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Default)]
921pub enum CyclePolicy {
922 #[default]
924 Error,
925 Iterate {
933 max_iterations: u32,
937 max_change: f64,
941 },
942}
943
944impl CyclePolicy {
945 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
947 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
949
950 pub fn iterate_excel_defaults() -> Self {
952 CyclePolicy::Iterate {
953 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
954 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
955 }
956 }
957}
958
959#[derive(Debug, Clone, Copy, PartialEq, Eq)]
960pub enum SheetIndexMode {
961 Eager,
963 Lazy,
965 FastBatch,
967}
968
969pub use formualizer_common::DateSystem;
970
971pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
973where
974 R: EvaluationContext + 'static,
975{
976 Engine::new(resolver, config)
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
981pub struct SpillConfig {
982 pub conflict_policy: SpillConflictPolicy,
984 pub tiebreaker: SpillTiebreaker,
986 pub bounds_policy: SpillBoundsPolicy,
988 pub buffer_mode: SpillBufferMode,
990 pub memory_budget_bytes: Option<u64>,
992 pub cancellation: SpillCancellationPolicy,
994 pub visibility: SpillVisibility,
996
997 pub max_spill_cells: u32,
1001}
1002
1003impl Default for SpillConfig {
1004 fn default() -> Self {
1005 Self {
1006 conflict_policy: SpillConflictPolicy::Error,
1007 tiebreaker: SpillTiebreaker::FirstWins,
1008 bounds_policy: SpillBoundsPolicy::Strict,
1009 buffer_mode: SpillBufferMode::ShadowBuffer,
1010 memory_budget_bytes: None,
1011 cancellation: SpillCancellationPolicy::Cooperative,
1012 visibility: SpillVisibility::OnCommit,
1013 max_spill_cells: 10_000,
1015 }
1016 }
1017}
1018
1019#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1020pub enum SpillConflictPolicy {
1021 Error,
1022 Preempt,
1023}
1024
1025#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1026pub enum SpillTiebreaker {
1027 FirstWins,
1028 EvaluationEpochAsc,
1029 AnchorAddressAsc,
1030 FunctionPriorityThenAddress,
1031}
1032
1033#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1034pub enum SpillBoundsPolicy {
1035 Strict,
1036 Truncate,
1037}
1038
1039#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1040pub enum SpillBufferMode {
1041 ShadowBuffer,
1042 PersistenceJournal,
1043}
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1046pub enum SpillCancellationPolicy {
1047 Cooperative,
1048 Strict,
1049}
1050
1051#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1052pub enum SpillVisibility {
1053 OnCommit,
1054 StagedLayer,
1055}
1056
1057#[derive(Debug, Default)]
1068pub struct TombstoneRegistry {
1069 pub pending_references: HashMap<String, Vec<VertexId>>,
1071}
1072
1073impl TombstoneRegistry {
1074 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1076 self.pending_references
1077 .entry(sheet_name)
1078 .or_default()
1079 .push(vertex_id);
1080 }
1081
1082 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1084 self.pending_references
1085 .remove(sheet_name)
1086 .unwrap_or_default()
1087 }
1088}