1pub mod arrow_ingest;
6pub mod effects;
7pub mod eval;
8pub mod eval_delta;
9pub mod graph;
10pub mod ingest;
11pub mod ingest_builder;
12pub mod journal;
13pub mod plan;
14pub mod range_view;
15pub mod row_visibility;
16pub mod scheduler;
17pub mod spill;
18pub mod vertex;
19pub mod virtual_deps;
20
21pub mod csr_edges;
23pub mod debug_views;
24pub mod delta_edges;
25pub mod interval_tree;
26pub mod named_range;
27pub mod sheet_index;
28pub mod sheet_registry;
29pub mod topo;
30pub mod vertex_store;
31
32pub mod arena;
34
35pub mod tuning;
37
38#[cfg(test)]
39mod tests;
40
41pub use eval::{Engine, EngineAction, EvalResult, RecalcPlan, VirtualDepTelemetry};
42pub use eval_delta::{DeltaMode, EvalDelta};
43pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
44pub use graph::snapshot::VertexSnapshot;
46pub use graph::{
47 ChangeEvent, DependencyGraph, DependencyRef, OperationSummary, StripeKey, StripeType,
48 block_index,
49};
50pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
51pub use scheduler::{Layer, Schedule, Scheduler};
52pub use vertex::{VertexId, VertexKind};
53
54pub use graph::editor::{
55 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
56 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
57};
58
59pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
60
61use crate::timezone::TimeZoneSpec;
64use crate::traits::EvaluationContext;
65use crate::traits::VolatileLevel;
66use chrono::{DateTime, Utc};
67use formualizer_common::error::{ExcelError, ExcelErrorKind};
68use std::collections::HashMap;
69
70impl<R: EvaluationContext> Engine<R> {
71 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
72 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
73 }
74}
75
76pub trait CalcObserver: Send + Sync {
78 fn on_eval_start(&self, vertex_id: VertexId);
79 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
80 fn on_cycle_detected(&self, cycle: &[VertexId]);
81 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
82}
83
84impl CalcObserver for () {
86 fn on_eval_start(&self, _vertex_id: VertexId) {}
87 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
88 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
89 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum DeterministicMode {
97 Disabled {
99 timezone: TimeZoneSpec,
101 },
102 Enabled {
104 timestamp_utc: DateTime<Utc>,
106 timezone: TimeZoneSpec,
108 },
109}
110
111impl Default for DeterministicMode {
112 fn default() -> Self {
113 Self::Disabled {
114 timezone: TimeZoneSpec::default(),
115 }
116 }
117}
118
119impl DeterministicMode {
120 pub fn is_enabled(&self) -> bool {
121 matches!(self, DeterministicMode::Enabled { .. })
122 }
123
124 pub fn timezone(&self) -> &TimeZoneSpec {
125 match self {
126 DeterministicMode::Disabled { timezone } => timezone,
127 DeterministicMode::Enabled { timezone, .. } => timezone,
128 }
129 }
130
131 pub fn validate(&self) -> Result<(), ExcelError> {
132 if let DeterministicMode::Enabled { timezone, .. } = self {
133 timezone
134 .validate_for_determinism()
135 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
136 }
137 Ok(())
138 }
139
140 pub fn build_clock(
141 &self,
142 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
143 self.validate()?;
144 Ok(match self {
145 #[cfg(feature = "system-clock")]
146 DeterministicMode::Disabled { timezone } => {
147 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
148 }
149 #[cfg(not(feature = "system-clock"))]
150 DeterministicMode::Disabled { timezone: _ } => {
151 std::sync::Arc::new(crate::timezone::FixedClock::new(
156 chrono::DateTime::UNIX_EPOCH,
157 crate::timezone::TimeZoneSpec::Utc,
158 ))
159 }
160 DeterministicMode::Enabled {
161 timestamp_utc,
162 timezone,
163 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
164 *timestamp_utc,
165 timezone.clone(),
166 )),
167 })
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum FormulaParsePolicy {
174 Strict,
176 CoerceToError,
178 KeepCachedValue,
180 AsText,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct FormulaParseDiagnostic {
187 pub sheet: String,
188 pub row: u32,
189 pub col: u32,
190 pub formula: String,
191 pub message: String,
192 pub policy: FormulaParsePolicy,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct WorkbookLoadLimits {
198 pub max_sheet_rows: u32,
200 pub max_sheet_cols: u32,
202 pub max_sheet_logical_cells: u64,
204 pub sparse_sheet_cell_threshold: u64,
206 pub max_sparse_cell_ratio: u64,
208}
209
210impl Default for WorkbookLoadLimits {
211 fn default() -> Self {
212 Self {
213 max_sheet_rows: 1_048_576,
214 max_sheet_cols: 16_384,
215 max_sheet_logical_cells: 128_000_000,
216 sparse_sheet_cell_threshold: 250_000,
217 max_sparse_cell_ratio: 1_024,
218 }
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct EvalConfig {
225 pub enable_parallel: bool,
226 pub max_threads: Option<usize>,
227 pub max_vertices: Option<usize>,
229 pub max_eval_time: Option<std::time::Duration>,
230 pub max_memory_mb: Option<usize>,
231
232 pub default_sheet_name: String,
234
235 pub case_sensitive_names: bool,
239
240 pub case_sensitive_tables: bool,
244
245 pub workbook_seed: u64,
247
248 pub volatile_level: VolatileLevel,
250
251 pub deterministic_mode: DeterministicMode,
253
254 pub range_expansion_limit: usize,
257
258 pub max_open_ended_rows: u32,
262
263 pub max_open_ended_cols: u32,
267
268 pub stripe_height: u32,
270 pub stripe_width: u32,
272 pub enable_block_stripes: bool,
274
275 pub spill: SpillConfig,
277
278 pub use_dynamic_topo: bool,
280 pub pk_visit_budget: usize,
282 pub pk_compaction_interval_ops: u64,
284 pub max_layer_width: Option<usize>,
286 pub pk_reject_cycle_edges: bool,
289 pub sheet_index_mode: SheetIndexMode,
291
292 pub warmup: tuning::WarmupConfig,
294
295 pub arrow_storage_enabled: bool,
297 pub delta_overlay_enabled: bool,
299
300 pub write_formula_overlay_enabled: bool,
303
304 pub max_overlay_memory_bytes: Option<usize>,
309
310 pub date_system: DateSystem,
312
313 pub formula_parse_policy: FormulaParsePolicy,
315
316 pub defer_graph_building: bool,
319
320 pub enable_virtual_dep_telemetry: bool,
324}
325
326impl Default for EvalConfig {
327 fn default() -> Self {
328 Self {
329 enable_parallel: true,
330 max_threads: None,
331 max_vertices: None,
332 max_eval_time: None,
333 max_memory_mb: None,
334
335 default_sheet_name: format!("Sheet{}", 1),
336
337 case_sensitive_names: false,
339 case_sensitive_tables: false,
340
341 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
343
344 volatile_level: VolatileLevel::Always,
346
347 deterministic_mode: DeterministicMode::default(),
348
349 range_expansion_limit: 64,
351 max_open_ended_rows: 1_048_576,
354 max_open_ended_cols: 16_384,
355 stripe_height: 256,
356 stripe_width: 256,
357 enable_block_stripes: false,
358 spill: SpillConfig::default(),
359
360 use_dynamic_topo: false, pk_visit_budget: 50_000,
363 pk_compaction_interval_ops: 100_000,
364 max_layer_width: None,
365 pk_reject_cycle_edges: false,
366 sheet_index_mode: SheetIndexMode::Eager,
367 warmup: tuning::WarmupConfig::default(),
368 arrow_storage_enabled: true,
369 delta_overlay_enabled: true,
370 write_formula_overlay_enabled: true,
371 max_overlay_memory_bytes: None,
372 date_system: DateSystem::Excel1900,
373 formula_parse_policy: FormulaParsePolicy::Strict,
374 defer_graph_building: false,
375 enable_virtual_dep_telemetry: false,
376 }
377 }
378}
379
380impl EvalConfig {
381 #[inline]
382 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
383 self.range_expansion_limit = limit;
384 self
385 }
386
387 #[inline]
388 pub fn with_parallel(mut self, enable: bool) -> Self {
389 self.enable_parallel = enable;
390 self
391 }
392
393 #[inline]
394 pub fn with_block_stripes(mut self, enable: bool) -> Self {
395 self.enable_block_stripes = enable;
396 self
397 }
398
399 #[inline]
400 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
401 self.case_sensitive_names = enable;
402 self
403 }
404
405 #[inline]
406 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
407 self.case_sensitive_tables = enable;
408 self
409 }
410
411 #[inline]
412 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
413 self.arrow_storage_enabled = enable;
414 self
415 }
416
417 #[inline]
418 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
419 self.delta_overlay_enabled = enable;
420 self
421 }
422
423 #[inline]
424 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
425 self.write_formula_overlay_enabled = enable;
426 self
427 }
428
429 #[inline]
430 pub fn with_date_system(mut self, system: DateSystem) -> Self {
431 self.date_system = system;
432 self
433 }
434
435 #[inline]
436 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
437 self.formula_parse_policy = policy;
438 self
439 }
440
441 #[inline]
442 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
443 self.enable_virtual_dep_telemetry = enable;
444 self
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum SheetIndexMode {
450 Eager,
452 Lazy,
454 FastBatch,
456}
457
458pub use formualizer_common::DateSystem;
459
460pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
462where
463 R: EvaluationContext + 'static,
464{
465 Engine::new(resolver, config)
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub struct SpillConfig {
471 pub conflict_policy: SpillConflictPolicy,
473 pub tiebreaker: SpillTiebreaker,
475 pub bounds_policy: SpillBoundsPolicy,
477 pub buffer_mode: SpillBufferMode,
479 pub memory_budget_bytes: Option<u64>,
481 pub cancellation: SpillCancellationPolicy,
483 pub visibility: SpillVisibility,
485
486 pub max_spill_cells: u32,
490}
491
492impl Default for SpillConfig {
493 fn default() -> Self {
494 Self {
495 conflict_policy: SpillConflictPolicy::Error,
496 tiebreaker: SpillTiebreaker::FirstWins,
497 bounds_policy: SpillBoundsPolicy::Strict,
498 buffer_mode: SpillBufferMode::ShadowBuffer,
499 memory_budget_bytes: None,
500 cancellation: SpillCancellationPolicy::Cooperative,
501 visibility: SpillVisibility::OnCommit,
502 max_spill_cells: 10_000,
504 }
505 }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
509pub enum SpillConflictPolicy {
510 Error,
511 Preempt,
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum SpillTiebreaker {
516 FirstWins,
517 EvaluationEpochAsc,
518 AnchorAddressAsc,
519 FunctionPriorityThenAddress,
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub enum SpillBoundsPolicy {
524 Strict,
525 Truncate,
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum SpillBufferMode {
530 ShadowBuffer,
531 PersistenceJournal,
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub enum SpillCancellationPolicy {
536 Cooperative,
537 Strict,
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum SpillVisibility {
542 OnCommit,
543 StagedLayer,
544}
545
546#[derive(Debug, Default)]
557pub struct TombstoneRegistry {
558 pub pending_references: HashMap<String, Vec<VertexId>>,
560}
561
562impl TombstoneRegistry {
563 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
565 self.pending_references
566 .entry(sheet_name)
567 .or_default()
568 .push(vertex_id);
569 }
570
571 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
573 self.pending_references
574 .remove(sheet_name)
575 .unwrap_or_default()
576 }
577}