1pub mod arrow_ingest;
6pub mod cancel;
7pub(crate) mod convergence;
8pub mod effects;
9pub mod eval;
10pub mod eval_delta;
11pub mod formula_ingest;
12mod formula_source;
13pub(crate) mod fragmented_transaction;
14pub mod graph;
15pub mod ingest;
16pub mod ingest_builder;
17pub(crate) mod ingest_pipeline;
18pub mod inspect;
19pub mod journal;
20pub mod live_edges;
21pub mod live_graph;
22pub mod lookup_index_cache;
23pub mod plan;
24#[cfg(test)]
25mod plan_legacy_tests;
26pub mod range_view;
27pub(crate) mod refs;
28pub mod resource_ledger;
29pub mod resource_observability;
30pub(crate) mod result_finalization;
31pub mod row_visibility;
32pub mod scheduler;
33pub mod spill;
34mod target_preparation;
35pub(crate) mod used_extent;
36pub mod vertex;
37pub mod virtual_deps;
38
39pub mod csr_edges;
41pub mod debug_views;
42pub mod delta_edges;
43pub mod interval_tree;
44pub mod named_range;
45pub mod sheet_index;
46pub mod sheet_registry;
47pub mod topo;
48pub mod vertex_store;
49
50pub mod arena;
52
53pub mod tuning;
55
56#[cfg(test)]
57mod tests;
58
59pub use arena::AstNodeId;
60pub use cancel::CancelToken;
61pub use eval::{
62 CycleTelemetry, Engine, EngineAction, EngineBaselineStats, EvalResult, RecalcPlan,
63 SourceFormulaIngress, TableMetadata, VirtualDepTelemetry,
64};
65pub use eval_delta::{
66 DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy, EvalDeltaRecord, TARGET_EVAL_DELTA_VERSION,
67 TargetEvalDelta,
68};
69pub use formula_ingest::{FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport};
70#[doc(hidden)]
71pub use formula_source::{
72 DeferredFormulaPackage, DeferredFormulaReplay, DeferredReplayFormula,
73 ExplicitPartitionLegacyMembers, ExplicitSourceFamilyMembers, FormulaCompressedPreparation,
74 FormulaCompressedSourceBatch, FormulaCompressedSourceReport,
75 FormulaReplayCoordinateDisposition, FormulaReplayDisposition, FormulaReplayPartitionRouter,
76 MAX_EXPLICIT_SOURCE_FAMILY_MEMBERS, MAX_PARTITIONED_SOURCE_FAMILY_FRAGMENTS,
77 PartitionLegacyMember, PartitionLegacyMemberKind, PartitionReconciliation,
78 PartitionedSourceFormulaFamily, PlacementDomainTransport, SourceCoord, SourceFamilyId,
79 SourceFamilyMembers, SourceFormulaFamily, SourceFormulaOrder, SourceRect,
80};
81pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
82#[allow(deprecated)]
83pub use target_preparation::PrepareTargetsOptions;
84pub use formualizer_common::{ResourceExhaustionDetail, ResourceExhaustionReason};
86pub use graph::snapshot::VertexSnapshot;
87pub use graph::{
88 ChangeEvent, DependencyGraph, DependencyRef, GraphBaselineStats, OperationSummary, StripeKey,
89 StripeType, block_index,
90};
91pub use resource_ledger::{
92 AdmissionResourceBudget, DeadlineResourceBudget, DiskScratchPolicy, EvaluationBudgets,
93 EvaluationIncompleteReason, EvaluationResourceConfigDiagnostic,
94 LegacyResourceConfigDisposition, OptimizationResourceBudget, ResourceEnvelope,
95 ResourceLedgerError, ResourceLedgerSnapshot, RetainedResourceBudget, ScratchResourceBudget,
96 SemanticResourceBudget, WorkResourceBudget,
97};
98pub(crate) use resource_ledger::ResourceLedger;
101pub use resource_observability::{
102 EvaluationRequestKind, EvaluationRequestOutcome, EvaluationRequestPhaseTimings,
103 EvaluationResourceBaselineStats, EvaluationResourceClass, EvaluationResourceLedgerRequestStats,
104 EvaluationResourceReason, EvaluationResourceRequestStats, FormulaDirtyLeaseOutcome,
105 FormulaPlaneRoute, FormulaPlaneRouteEvent, FormulaPlaneRoutePhase,
106 FormulaPlaneRouteTransitionReason, FormulaPlaneTopologyCacheOutcome,
107 FormulaPlaneTopologyRequestStats, FormulaPlaneTopologyStrategy,
108};
109pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
110pub use scheduler::{Layer, Schedule, ScheduleUnit, Scheduler};
111pub use target_preparation::{
112 EvaluationTarget, OpaquePreparePolicy, OpaqueReason, PreparationOutcome, PreparationRevision,
113 PrepareScope, PreparedTargetGraphReport, RequestId, TableSelection, TargetEvalOptions,
114};
115pub use vertex::{VertexId, VertexKind};
116
117pub use graph::editor::{
118 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
119 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
120};
121
122pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
123
124#[doc(hidden)]
125pub mod fp8_parity_test_support {
126 use super::{Engine, EvalConfig};
127 use crate::engine::arena::CanonicalLabels;
128 use crate::formula_plane::dependency_summary::summarize_canonical_template;
129 use crate::formula_plane::producer::SpanReadSummary;
130 use crate::formula_plane::runtime::{PlacementDomain, ResultRegion};
131 use crate::formula_plane::template_canonical::{
132 CanonicalRejectReason, CanonicalTemplateFlag, canonicalize_template,
133 };
134 use crate::reference::{CellRef, Coord};
135 use crate::traits::EvaluationContext;
136 use formualizer_common::{ExcelError, LiteralValue};
137 use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, parse};
138 use std::collections::BTreeSet;
139 use std::sync::Arc;
140
141 #[derive(Clone, Debug)]
142 pub struct Fp8ParityObservation {
143 pub formula: String,
144 pub placement: CellRef,
145 pub old_payload: String,
146 pub new_hash: u64,
147 }
148
149 pub fn default_config() -> EvalConfig {
150 EvalConfig::default()
151 }
152
153 pub fn parse_formula(formula: &str) -> ASTNode {
154 parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"))
155 }
156
157 pub fn cell(sheet_id: u16, row: u32, col: u32) -> CellRef {
158 CellRef::new(sheet_id, Coord::from_excel(row, col, true, true))
159 }
160
161 fn local_binding_declarations(ast: &ASTNode, out: &mut BTreeSet<String>) {
162 match &ast.node_type {
163 ASTNodeType::Function { name, args } => {
164 let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
165 let declaration_indices: Box<dyn Iterator<Item = usize>> = match canonical.as_str()
166 {
167 "LET" => Box::new((0..args.len().saturating_sub(1)).step_by(2)),
168 "LAMBDA" => Box::new(0..args.len().saturating_sub(1)),
169 _ => Box::new(std::iter::empty()),
170 };
171 for index in declaration_indices {
172 if let Some(ASTNode {
173 node_type:
174 ASTNodeType::Reference {
175 reference: ReferenceType::NamedRange(name),
176 ..
177 },
178 ..
179 }) = args.get(index)
180 {
181 out.insert(name.to_ascii_uppercase());
182 }
183 }
184 for arg in args {
185 local_binding_declarations(arg, out);
186 }
187 }
188 ASTNodeType::UnaryOp { expr, .. } => local_binding_declarations(expr, out),
189 ASTNodeType::BinaryOp { left, right, .. } => {
190 local_binding_declarations(left, out);
191 local_binding_declarations(right, out);
192 }
193 ASTNodeType::Call { callee, args } => {
194 local_binding_declarations(callee, out);
195 for arg in args {
196 local_binding_declarations(arg, out);
197 }
198 }
199 ASTNodeType::Array(rows) => {
200 for item in rows.iter().flatten() {
201 local_binding_declarations(item, out);
202 }
203 }
204 _ => {}
205 }
206 }
207
208 pub fn assert_case<R: EvaluationContext>(
209 engine: &mut Engine<R>,
210 formula: &str,
211 placement: CellRef,
212 ) -> Fp8ParityObservation {
213 let parsed = parse_formula(formula);
214 assert_case_ast(engine, formula, parsed, placement)
215 }
216
217 pub fn assert_case_ast<R: EvaluationContext>(
218 engine: &mut Engine<R>,
219 formula: &str,
220 parsed: ASTNode,
221 placement: CellRef,
222 ) -> Fp8ParityObservation {
223 let mut local_declarations = BTreeSet::new();
224 local_binding_declarations(&parsed, &mut local_declarations);
225 let mut old_ast = parsed.clone();
226 let old_rewrite = engine
227 .graph
228 .rewrite_structured_references_for_cell(&mut old_ast, placement);
229 let old = old_rewrite.and_then(|_| old_path(engine, &old_ast, placement));
230
231 let new = {
232 let mut pipeline = engine.ingest_pipeline();
233 pipeline.ingest_formula(
234 crate::engine::ingest_pipeline::FormulaAstInput::Tree(parsed),
235 placement,
236 Some(Arc::<str>::from(formula)),
237 )
238 };
239
240 match (old, new) {
241 (Ok(old), Ok(new)) => {
242 let new_direct = sorted_cells(new.dep_plan.direct_cell_deps.clone());
243 assert_eq!(
244 old.direct_cells, new_direct,
245 "direct deps differ for {formula} at {placement:?}\nold={:?}\nnew={:?}",
246 old.direct_cells, new_direct
247 );
248 assert_eq!(
249 old.range_deps, new.dep_plan.range_deps,
250 "range deps differ for {formula} at {placement:?}"
251 );
252 let old_unresolved_names: Vec<_> = old
253 .unresolved_names
254 .iter()
255 .filter(|name| !local_declarations.contains(&name.to_ascii_uppercase()))
256 .cloned()
257 .collect();
258 assert_eq!(
259 old_unresolved_names, new.dep_plan.named_refs,
260 "unresolved names differ for {formula} at {placement:?}"
261 );
262 assert_eq!(
263 old.volatile, new.dep_plan.volatile,
264 "volatile flag differs for {formula} at {placement:?}"
265 );
266 assert_eq!(
267 old.dynamic, new.dep_plan.dynamic,
268 "dynamic flag differs for {formula} at {placement:?}"
269 );
270 let mut expected_labels = canonical_labels_from_old(&old.labels);
271 if old.dynamic {
272 expected_labels.flags |= CanonicalLabels::FLAG_DYNAMIC;
273 }
274 assert_eq!(
275 expected_labels.flags, new.labels.flags,
276 "canonical label flags differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
277 old.labels.flags, new.labels.flags
278 );
279 assert_eq!(
280 expected_labels.rejects, new.labels.rejects,
281 "canonical label rejects differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
282 old.labels.reject_reasons, new.labels.rejects
283 );
284 let named_resolution_superset = old.summary_rejected_only_for_named_reference
290 && old.read_summary_debug.is_none();
291 if !named_resolution_superset {
292 assert_eq!(
293 old.read_summary_debug,
294 new.read_summary.as_ref().map(|s| format!("{s:?}")),
295 "read summary differs for {formula} at {placement:?}"
296 );
297 }
298 assert_eq!(new.formula_text.as_deref(), Some(formula));
299 assert_eq!(new.placement, placement);
300 Fp8ParityObservation {
301 formula: formula.to_string(),
302 placement,
303 old_payload: old.payload,
304 new_hash: new.canonical_hash,
305 }
306 }
307 (Err(old), Err(new)) => {
308 assert_eq!(
309 old.kind.to_string(),
310 new.kind.to_string(),
311 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
312 );
313 Fp8ParityObservation {
314 formula: formula.to_string(),
315 placement,
316 old_payload: format!("ERR:{:?}", old.kind),
317 new_hash: 0,
318 }
319 }
320 (Ok(_), Err(new)) => panic!(
321 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
322 ),
323 (Err(old), Ok(_)) => panic!(
324 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
325 ),
326 }
327 }
328
329 #[derive(Debug)]
330 struct OldOutput {
331 payload: String,
332 labels: crate::formula_plane::template_canonical::CanonicalTemplateLabels,
333 direct_cells: Vec<CellRef>,
334 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
335 unresolved_names: Vec<String>,
336 volatile: bool,
337 dynamic: bool,
338 read_summary_debug: Option<String>,
339 summary_rejected_only_for_named_reference: bool,
340 }
341
342 fn old_path<R: EvaluationContext>(
343 engine: &mut Engine<R>,
344 ast: &ASTNode,
345 placement: CellRef,
346 ) -> Result<OldOutput, ExcelError> {
347 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
348 .graph
349 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
350 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
351 let dynamic = engine.graph.is_ast_dynamic(ast);
352 let template =
353 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
354 let summary = summarize_canonical_template(&template);
355 let scalar_domain = PlacementDomain::row_run(
356 placement.sheet_id,
357 placement.coord.row(),
358 placement.coord.row(),
359 placement.coord.col(),
360 );
361 let result_region = ResultRegion::scalar_cells(scalar_domain);
362 let read_summary = SpanReadSummary::from_formula_summary(
363 placement.sheet_id,
364 &result_region,
365 &summary,
366 engine.graph.sheet_reg(),
367 )
368 .ok();
369 let summary_rejected_only_for_named_reference = !summary.reject_reasons.is_empty()
370 && summary.reject_reasons.iter().all(|reason| {
371 matches!(
372 reason,
373 crate::formula_plane::dependency_summary::DependencyRejectReason
374 ::NamedRangeUnsupported { .. }
375 )
376 });
377 Ok(OldOutput {
378 payload: template.key.payload().to_string(),
379 labels: template.labels,
380 direct_cells: sorted_cells(placeholders),
381 range_deps: ranges,
382 unresolved_names,
383 volatile,
384 dynamic,
385 read_summary_debug: read_summary.as_ref().map(|s| format!("{s:?}")),
386 summary_rejected_only_for_named_reference,
387 })
388 }
389
390 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
391 cells.sort();
392 cells.dedup();
393 cells
394 }
395
396 fn canonical_labels_from_old(
397 old: &crate::formula_plane::template_canonical::CanonicalTemplateLabels,
398 ) -> CanonicalLabels {
399 let mut labels = CanonicalLabels::default();
400 for flag in &old.flags {
401 labels.flags |= match flag {
402 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
403 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
404 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
405 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
406 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
407 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
408 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
409 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
410 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
411 };
412 }
413 for reason in &old.reject_reasons {
414 labels.flags |= match reason {
415 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
416 CanonicalLabels::FLAG_DYNAMIC
417 }
418 CanonicalRejectReason::ParserVolatileFlag
419 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
420 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
421 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
422 }
423 CanonicalRejectReason::ArrayOrSpillFunction { .. }
424 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
425 CanonicalRejectReason::StructuredReference { .. }
426 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
427 CanonicalLabels::FLAG_CONTAINS_TABLE
428 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
429 }
430 CanonicalRejectReason::OpenRangeReference { .. }
431 | CanonicalRejectReason::WholeAxisReference { .. } => {
432 CanonicalLabels::FLAG_CONTAINS_RANGE
433 }
434 _ => 0,
435 };
436 labels.rejects |= match reason {
437 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
438 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
439 }
440 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
441 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
442 }
443 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
444 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
445 }
446 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
447 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
448 }
449 CanonicalRejectReason::ParserVolatileFlag => {
450 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
451 }
452 CanonicalRejectReason::VolatileFunction { .. } => {
453 CanonicalLabels::REJECT_VOLATILE_FUNCTION
454 }
455 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
456 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
457 }
458 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
459 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
460 }
461 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
462 CanonicalRejectReason::SpillReference { .. } => {
463 CanonicalLabels::REJECT_SPILL_REFERENCE
464 }
465 CanonicalRejectReason::SpillResultRegionOperator => {
466 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
467 }
468 CanonicalRejectReason::ImplicitIntersectionOperator => {
469 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
470 }
471 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
472 CanonicalRejectReason::StructuredReference { .. } => {
473 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
474 }
475 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
476 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
477 }
478 CanonicalRejectReason::ThreeDReference { .. } => {
479 CanonicalLabels::REJECT_THREE_D_REFERENCE
480 }
481 CanonicalRejectReason::ExternalReference { .. } => {
482 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
483 }
484 CanonicalRejectReason::OpenRangeReference { .. } => {
485 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
486 }
487 CanonicalRejectReason::WholeAxisReference { .. } => {
488 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
489 }
490 CanonicalRejectReason::UnsupportedReference { .. } => {
491 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
492 }
493 CanonicalRejectReason::FunctionContractUnsupported { .. }
494 | CanonicalRejectReason::ContextDependentFunction { .. } => {
495 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
496 }
497 };
498 }
499 labels
500 }
501
502 pub fn literal_number(value: f64) -> LiteralValue {
503 LiteralValue::Number(value)
504 }
505}
506
507use crate::timezone::TimeZoneSpec;
510use crate::traits::EvaluationContext;
511use crate::traits::VolatileLevel;
512use chrono::{DateTime, Utc};
513use formualizer_common::error::{ExcelError, ExcelErrorKind};
514use std::collections::HashMap;
515
516impl<R: EvaluationContext> Engine<R> {
517 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
518 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
519 }
520
521 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
522 self.graph.store_ast(ast)
523 }
524}
525
526pub trait CalcObserver: Send + Sync {
528 fn on_eval_start(&self, vertex_id: VertexId);
529 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
530 fn on_cycle_detected(&self, cycle: &[VertexId]);
531 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
532}
533
534impl CalcObserver for () {
536 fn on_eval_start(&self, _vertex_id: VertexId) {}
537 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
538 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
539 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
546pub enum DeterministicMode {
547 Disabled {
549 timezone: TimeZoneSpec,
551 },
552 Enabled {
554 timestamp_utc: DateTime<Utc>,
556 timezone: TimeZoneSpec,
558 },
559}
560
561impl Default for DeterministicMode {
562 fn default() -> Self {
563 Self::Disabled {
564 timezone: TimeZoneSpec::default(),
565 }
566 }
567}
568
569impl DeterministicMode {
570 pub fn is_enabled(&self) -> bool {
571 matches!(self, DeterministicMode::Enabled { .. })
572 }
573
574 pub fn timezone(&self) -> &TimeZoneSpec {
575 match self {
576 DeterministicMode::Disabled { timezone } => timezone,
577 DeterministicMode::Enabled { timezone, .. } => timezone,
578 }
579 }
580
581 pub fn validate(&self) -> Result<(), ExcelError> {
582 if let DeterministicMode::Enabled { timezone, .. } = self {
583 timezone
584 .validate_for_determinism()
585 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
586 }
587 Ok(())
588 }
589
590 pub fn build_clock(
591 &self,
592 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
593 self.validate()?;
594 Ok(match self {
595 #[cfg(feature = "system-clock")]
596 DeterministicMode::Disabled { timezone } => {
597 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
598 }
599 #[cfg(not(feature = "system-clock"))]
600 DeterministicMode::Disabled { timezone: _ } => {
601 std::sync::Arc::new(crate::timezone::FixedClock::new(
606 chrono::DateTime::UNIX_EPOCH,
607 crate::timezone::TimeZoneSpec::Utc,
608 ))
609 }
610 DeterministicMode::Enabled {
611 timestamp_utc,
612 timezone,
613 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
614 *timestamp_utc,
615 timezone.clone(),
616 )),
617 })
618 }
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub enum FormulaParsePolicy {
624 Strict,
626 CoerceToError,
628 KeepCachedValue,
630 AsText,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct FormulaParseDiagnostic {
637 pub sheet: String,
638 pub row: u32,
639 pub col: u32,
640 pub formula: String,
641 pub message: String,
642 pub policy: FormulaParsePolicy,
643}
644
645#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
646pub enum FormulaPlaneMode {
647 #[default]
650 Off,
651 Shadow,
652 AuthoritativeExperimental,
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
659pub enum FormulaSpoolDiskPolicy {
660 NativeSpill,
662 MemoryOnly,
664}
665
666#[derive(Debug, Clone, PartialEq, Eq)]
668pub struct WorkbookLoadLimits {
669 pub max_sheet_rows: u32,
671 pub max_sheet_cols: u32,
673 pub max_sheet_logical_cells: u64,
675 pub max_formula_plane_fallback_cells: u64,
677 pub sparse_sheet_cell_threshold: u64,
679 pub max_sparse_cell_ratio: u64,
681 pub max_formula_spool_bytes_per_sheet: u64,
683 pub max_formula_spool_bytes_per_workbook: u64,
685 pub max_formula_spool_files_per_workbook: u32,
687 pub formula_spool_memory_prefix_bytes: u64,
689 pub max_formula_spool_memory_bytes: u64,
691 pub formula_spool_disk_policy: FormulaSpoolDiskPolicy,
693}
694
695impl Default for WorkbookLoadLimits {
696 fn default() -> Self {
697 Self {
698 max_sheet_rows: 1_048_576,
699 max_sheet_cols: 16_384,
700 max_sheet_logical_cells: 128_000_000,
701 max_formula_plane_fallback_cells: 2_000_000,
702 sparse_sheet_cell_threshold: 250_000,
703 max_sparse_cell_ratio: 1_024,
704 max_formula_spool_bytes_per_sheet: 256 * 1024 * 1024,
705 max_formula_spool_bytes_per_workbook: 1024 * 1024 * 1024,
706 max_formula_spool_files_per_workbook: 1_024,
707 formula_spool_memory_prefix_bytes: 1024 * 1024,
708 max_formula_spool_memory_bytes: 16 * 1024 * 1024,
709 formula_spool_disk_policy: if cfg!(target_arch = "wasm32") {
710 FormulaSpoolDiskPolicy::MemoryOnly
711 } else {
712 FormulaSpoolDiskPolicy::NativeSpill
713 },
714 }
715 }
716}
717
718#[derive(Debug, Clone)]
720pub struct EvalConfig {
721 pub enable_parallel: bool,
722 pub max_threads: Option<usize>,
723 pub max_vertices: Option<usize>,
726 pub max_eval_time: Option<std::time::Duration>,
729 pub max_memory_mb: Option<usize>,
733 pub evaluation_budgets: EvaluationBudgets,
737
738 pub default_sheet_name: String,
740
741 pub case_sensitive_names: bool,
745
746 pub case_sensitive_tables: bool,
750
751 pub workbook_seed: u64,
753
754 pub volatile_level: VolatileLevel,
756
757 pub deterministic_mode: DeterministicMode,
759
760 pub range_expansion_limit: usize,
763
764 pub max_open_ended_rows: u32,
768
769 pub max_open_ended_cols: u32,
773
774 pub stripe_height: u32,
776 pub stripe_width: u32,
778 pub enable_block_stripes: bool,
780
781 pub spill: SpillConfig,
783
784 pub cycle: CycleConfig,
788
789 pub use_dynamic_topo: bool,
791 pub pk_visit_budget: usize,
793 pub pk_compaction_interval_ops: u64,
795 pub max_layer_width: Option<usize>,
797 pub pk_reject_cycle_edges: bool,
800 pub sheet_index_mode: SheetIndexMode,
802
803 pub warmup: tuning::WarmupConfig,
805
806 pub arrow_storage_enabled: bool,
808 pub delta_overlay_enabled: bool,
810
811 pub write_formula_overlay_enabled: bool,
814
815 pub max_overlay_memory_bytes: Option<usize>,
820
821 pub date_system: DateSystem,
823
824 pub formula_parse_policy: FormulaParsePolicy,
826
827 pub defer_graph_building: bool,
830
831 pub enable_virtual_dep_telemetry: bool,
835
836 pub formula_plane_mode: FormulaPlaneMode,
841 pub max_formula_plane_cache_candidates: usize,
843 pub max_formula_plane_cache_edges: usize,
845 pub max_formula_plane_cache_bytes: usize,
847
848 pub lookup_index_cache_max_bytes: usize,
850}
851
852impl Default for EvalConfig {
853 fn default() -> Self {
854 Self {
855 enable_parallel: true,
856 max_threads: None,
857 max_vertices: None,
858 max_eval_time: None,
859 max_memory_mb: None,
860 evaluation_budgets: EvaluationBudgets::default(),
861
862 default_sheet_name: format!("Sheet{}", 1),
863
864 case_sensitive_names: false,
866 case_sensitive_tables: false,
867
868 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
870
871 volatile_level: VolatileLevel::Always,
873
874 deterministic_mode: DeterministicMode::default(),
875
876 range_expansion_limit: 64,
878 max_open_ended_rows: 1_048_576,
881 max_open_ended_cols: 16_384,
882 stripe_height: 256,
883 stripe_width: 256,
884 enable_block_stripes: false,
885 spill: SpillConfig::default(),
886 cycle: CycleConfig::default(),
887
888 use_dynamic_topo: false, pk_visit_budget: 50_000,
891 pk_compaction_interval_ops: 100_000,
892 max_layer_width: None,
893 pk_reject_cycle_edges: false,
894 sheet_index_mode: SheetIndexMode::Eager,
895 warmup: tuning::WarmupConfig::default(),
896 arrow_storage_enabled: true,
897 delta_overlay_enabled: true,
898 write_formula_overlay_enabled: true,
899 max_overlay_memory_bytes: None,
900 date_system: DateSystem::Excel1900,
901 formula_parse_policy: FormulaParsePolicy::Strict,
902 defer_graph_building: false,
903 enable_virtual_dep_telemetry: false,
904 formula_plane_mode: FormulaPlaneMode::Off,
905 max_formula_plane_cache_candidates: 100_000,
906 max_formula_plane_cache_edges: 100_000,
907 max_formula_plane_cache_bytes: 64 * 1024 * 1024,
908 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
909 }
910 }
911}
912
913impl EvalConfig {
914 #[inline]
915 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
916 self.range_expansion_limit = limit;
917 self
918 }
919
920 #[inline]
921 pub fn with_parallel(mut self, enable: bool) -> Self {
922 self.enable_parallel = enable;
923 self
924 }
925
926 #[inline]
927 pub fn with_block_stripes(mut self, enable: bool) -> Self {
928 self.enable_block_stripes = enable;
929 self
930 }
931
932 #[inline]
933 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
934 self.case_sensitive_names = enable;
935 self
936 }
937
938 #[inline]
939 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
940 self.case_sensitive_tables = enable;
941 self
942 }
943
944 #[inline]
945 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
946 self.arrow_storage_enabled = enable;
947 self
948 }
949
950 #[inline]
951 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
952 self.delta_overlay_enabled = enable;
953 self
954 }
955
956 #[inline]
957 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
958 self.write_formula_overlay_enabled = enable;
959 self
960 }
961
962 #[inline]
963 pub fn with_date_system(mut self, system: DateSystem) -> Self {
964 self.date_system = system;
965 self
966 }
967
968 #[inline]
969 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
970 self.formula_parse_policy = policy;
971 self
972 }
973
974 #[inline]
975 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
976 self.enable_virtual_dep_telemetry = enable;
977 self
978 }
979
980 #[inline]
981 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
982 self.formula_plane_mode = mode;
983 self
984 }
985
986 #[inline]
987 pub fn with_evaluation_budgets(mut self, budgets: EvaluationBudgets) -> Self {
988 self.evaluation_budgets = budgets;
989 self
990 }
991
992 pub fn resolved_evaluation_budgets(&self) -> EvaluationBudgets {
994 resource_ledger::resolve_evaluation_budgets(
995 &self.evaluation_budgets,
996 self.max_vertices,
997 self.max_memory_mb,
998 self.max_eval_time,
999 )
1000 .budgets
1001 }
1002
1003 #[inline]
1012 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
1013 if let Err(msg) = cycle.validate() {
1014 panic!("invalid CycleConfig: {msg}");
1015 }
1016 self.cycle = cycle;
1017 self
1018 }
1019}
1020
1021#[derive(Debug, Clone, Copy, PartialEq, Default)]
1026pub struct CycleConfig {
1027 pub detection: CycleDetection,
1028 pub policy: CyclePolicy,
1029}
1030
1031impl CycleConfig {
1032 pub fn iterate_excel_defaults() -> Self {
1035 Self {
1036 detection: CycleDetection::Runtime,
1037 policy: CyclePolicy::iterate_excel_defaults(),
1038 }
1039 }
1040
1041 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
1043 Self {
1044 detection: CycleDetection::Runtime,
1045 policy: CyclePolicy::Iterate {
1046 max_iterations,
1047 max_change,
1048 },
1049 }
1050 }
1051
1052 pub fn validate(&self) -> Result<(), String> {
1056 if let CyclePolicy::Iterate {
1057 max_iterations,
1058 max_change,
1059 } = self.policy
1060 {
1061 if self.detection == CycleDetection::Static {
1062 return Err(
1063 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
1064 );
1065 }
1066 if max_iterations == 0 {
1067 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
1068 }
1069 if !max_change.is_finite() || max_change < 0.0 {
1070 return Err(format!(
1071 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
1072 ));
1073 }
1074 }
1075 Ok(())
1076 }
1077
1078 #[inline]
1083 pub(crate) fn allows_self_dependency(&self) -> bool {
1084 self.detection == CycleDetection::Runtime
1085 && matches!(self.policy, CyclePolicy::Iterate { .. })
1086 }
1087}
1088
1089#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1091pub enum CycleDetection {
1092 #[default]
1095 Static,
1096 Runtime,
1100}
1101
1102#[derive(Debug, Clone, Copy, PartialEq, Default)]
1104pub enum CyclePolicy {
1105 #[default]
1107 Error,
1108 Iterate {
1116 max_iterations: u32,
1120 max_change: f64,
1124 },
1125}
1126
1127impl CyclePolicy {
1128 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1130 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1132
1133 pub fn iterate_excel_defaults() -> Self {
1135 CyclePolicy::Iterate {
1136 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
1137 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
1138 }
1139 }
1140}
1141
1142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1143pub enum SheetIndexMode {
1144 Eager,
1146 Lazy,
1148 FastBatch,
1150}
1151
1152pub use formualizer_common::DateSystem;
1153
1154pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1156where
1157 R: EvaluationContext + 'static,
1158{
1159 Engine::new(resolver, config)
1160}
1161
1162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1164pub struct SpillConfig {
1165 pub conflict_policy: SpillConflictPolicy,
1167 pub tiebreaker: SpillTiebreaker,
1169 pub bounds_policy: SpillBoundsPolicy,
1171 pub buffer_mode: SpillBufferMode,
1173 pub memory_budget_bytes: Option<u64>,
1175 pub cancellation: SpillCancellationPolicy,
1177 pub visibility: SpillVisibility,
1179
1180 pub max_spill_cells: u32,
1184}
1185
1186impl Default for SpillConfig {
1187 fn default() -> Self {
1188 Self {
1189 conflict_policy: SpillConflictPolicy::Error,
1190 tiebreaker: SpillTiebreaker::FirstWins,
1191 bounds_policy: SpillBoundsPolicy::Strict,
1192 buffer_mode: SpillBufferMode::ShadowBuffer,
1193 memory_budget_bytes: None,
1194 cancellation: SpillCancellationPolicy::Cooperative,
1195 visibility: SpillVisibility::OnCommit,
1196 max_spill_cells: 10_000,
1198 }
1199 }
1200}
1201
1202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1203pub enum SpillConflictPolicy {
1204 Error,
1205 Preempt,
1206}
1207
1208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1209pub enum SpillTiebreaker {
1210 FirstWins,
1211 EvaluationEpochAsc,
1212 AnchorAddressAsc,
1213 FunctionPriorityThenAddress,
1214}
1215
1216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1217pub enum SpillBoundsPolicy {
1218 Strict,
1219 Truncate,
1220}
1221
1222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1223pub enum SpillBufferMode {
1224 ShadowBuffer,
1225 PersistenceJournal,
1226}
1227
1228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1229pub enum SpillCancellationPolicy {
1230 Cooperative,
1231 Strict,
1232}
1233
1234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1235pub enum SpillVisibility {
1236 OnCommit,
1237 StagedLayer,
1238}
1239
1240#[derive(Debug, Default)]
1251pub struct TombstoneRegistry {
1252 pub pending_references: HashMap<String, Vec<VertexId>>,
1254}
1255
1256impl TombstoneRegistry {
1257 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1259 self.pending_references
1260 .entry(sheet_name)
1261 .or_default()
1262 .push(vertex_id);
1263 }
1264
1265 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1267 self.pending_references
1268 .remove(sheet_name)
1269 .unwrap_or_default()
1270 }
1271}