1pub mod addr;
6pub mod arrow_ingest;
7#[doc(hidden)]
10pub mod authority;
11pub mod cancel;
12pub(crate) mod convergence;
13pub(crate) mod derived_formats;
14pub mod effects;
15pub mod eval;
16pub mod eval_delta;
17pub mod formula_ingest;
18mod formula_source;
19pub mod graph;
20pub(crate) mod idset;
21pub mod ingest;
22pub mod ingest_builder;
23pub(crate) mod ingest_pipeline;
24pub mod inspect;
25pub mod journal;
26pub mod live_edges;
27pub mod live_graph;
28pub mod lookup_index_cache;
29pub mod plan;
30#[cfg(test)]
31mod plan_legacy_tests;
32pub mod range_view;
33pub(crate) mod refs;
34pub mod resource_ledger;
35pub mod resource_observability;
36pub(crate) mod result_finalization;
37pub mod row_visibility;
38pub mod scheduler;
39pub(crate) mod shape_memo;
40pub mod spill;
41mod target_preparation;
42#[doc(hidden)]
43pub mod template;
44pub(crate) mod used_extent;
45pub mod vertex;
46pub mod virtual_deps;
47
48#[cfg(any(test, feature = "legacy_oracle"))]
52pub mod csr_edges;
53pub mod debug_views;
54#[cfg(any(test, feature = "legacy_oracle"))]
57pub mod delta_edges;
58pub mod interval_tree;
59pub mod named_range;
60pub mod sheet_index;
61pub mod sheet_registry;
62#[cfg(any(test, feature = "legacy_oracle"))]
65pub mod topo;
66pub(crate) mod trace;
67pub mod vertex_store;
68
69pub mod arena;
71
72pub mod tuning;
74
75#[cfg(test)]
76mod tests;
77
78pub use arena::AstNodeId;
79pub use cancel::CancelToken;
80pub use eval::{
81 CycleTelemetry, Engine, EngineAction, EngineBaselineStats, EvalResult, RecalcPlan,
82 SourceFormulaIngress, TableMetadata, VirtualDepTelemetry,
83};
84pub use eval_delta::{
85 DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy, EvalDeltaRecord, TARGET_EVAL_DELTA_VERSION,
86 TargetEvalDelta,
87};
88pub use formula_ingest::{
89 FormulaFamilyGrouper, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
90};
91#[doc(hidden)]
92pub use formula_source::{
93 DeferredFormulaPackage, DeferredFormulaReplay, DeferredReplayFormula,
94 ExplicitPartitionLegacyMembers, ExplicitSourceFamilyMembers, FormulaCompressedPreparation,
95 FormulaCompressedSourceBatch, FormulaCompressedSourceReport,
96 FormulaReplayCoordinateDisposition, FormulaReplayDisposition, FormulaReplayPartitionRouter,
97 MAX_EXPLICIT_SOURCE_FAMILY_MEMBERS, MAX_PARTITIONED_SOURCE_FAMILY_FRAGMENTS,
98 PartitionLegacyMember, PartitionLegacyMemberKind, PartitionReconciliation,
99 PartitionedSourceFormulaFamily, PlacementDomainTransport, SourceCoord, SourceFamilyId,
100 SourceFamilyMembers, SourceFormulaFamily, SourceFormulaOrder, SourceRect,
101};
102pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
103#[allow(deprecated)]
104pub use target_preparation::PrepareTargetsOptions;
105pub use formualizer_common::{ResourceExhaustionDetail, ResourceExhaustionReason};
107pub use graph::FormulaView;
108pub use graph::snapshot::VertexSnapshot;
109pub use graph::{
110 ChangeEvent, DependencyGraph, DependencyRef, GraphBaselineStats, OperationSummary, StripeKey,
111 StripeType, block_index,
112};
113pub use resource_ledger::{
114 AdmissionResourceBudget, DeadlineResourceBudget, DiskScratchPolicy, EvaluationBudgets,
115 EvaluationIncompleteReason, EvaluationResourceConfigDiagnostic,
116 LegacyResourceConfigDisposition, OptimizationResourceBudget, ResourceEnvelope,
117 ResourceLedgerError, ResourceLedgerSnapshot, RetainedResourceBudget, ScratchResourceBudget,
118 SemanticResourceBudget, WorkResourceBudget,
119};
120pub(crate) use resource_ledger::ResourceLedger;
123pub use resource_observability::{
124 EvaluationRequestKind, EvaluationRequestOutcome, EvaluationRequestPhaseTimings,
125 EvaluationResourceBaselineStats, EvaluationResourceClass, EvaluationResourceLedgerRequestStats,
126 EvaluationResourceReason, EvaluationResourceRequestStats, FormulaDirtyLeaseOutcome,
127 FormulaPlaneRoute, FormulaPlaneRouteEvent, FormulaPlaneRoutePhase,
128 FormulaPlaneRouteTransitionReason, FormulaPlaneTopologyCacheOutcome,
129 FormulaPlaneTopologyRequestStats, FormulaPlaneTopologyStrategy,
130};
131pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
132#[cfg(any(test, feature = "legacy_oracle"))]
135pub use scheduler::Scheduler;
136pub use scheduler::{Layer, Schedule, ScheduleUnit};
137pub use target_preparation::{
138 EvaluationTarget, OpaquePreparePolicy, OpaqueReason, PreparationOutcome, PreparationRevision,
139 PrepareScope, PreparedTargetGraphReport, RequestId, TableSelection, TargetEvalOptions,
140};
141pub use vertex::{VertexId, VertexKind};
142
143pub use graph::editor::{
144 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
145 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
146};
147
148pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
149
150#[doc(hidden)]
151pub mod fp8_parity_test_support {
152 use super::{Engine, EvalConfig};
153 use crate::engine::arena::CanonicalLabels;
154 use crate::engine::template::canonical::{
155 CanonicalRejectReason, CanonicalTemplateFlag, canonicalize_template,
156 };
157 use crate::engine::template::dependency_summary::summarize_canonical_template;
158 use crate::engine::template::domain::{PlacementDomain, ResultRegion};
159 use crate::engine::template::read_summary::SpanReadSummary;
160 use crate::reference::{CellRef, Coord};
161 use crate::traits::EvaluationContext;
162 use formualizer_common::{ExcelError, LiteralValue};
163 use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, parse};
164 use std::collections::BTreeSet;
165 use std::sync::Arc;
166
167 #[derive(Clone, Debug)]
168 pub struct Fp8ParityObservation {
169 pub formula: String,
170 pub placement: CellRef,
171 pub old_payload: String,
172 pub new_hash: u64,
173 }
174
175 pub fn default_config() -> EvalConfig {
176 EvalConfig::default()
177 }
178
179 pub fn parse_formula(formula: &str) -> ASTNode {
180 parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"))
181 }
182
183 pub fn cell(sheet_id: u16, row: u32, col: u32) -> CellRef {
184 CellRef::new(sheet_id, Coord::from_excel(row, col, true, true))
185 }
186
187 fn local_binding_declarations(ast: &ASTNode, out: &mut BTreeSet<String>) {
188 match &ast.node_type {
189 ASTNodeType::Function { name, args } => {
190 let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
191 let declaration_indices: Box<dyn Iterator<Item = usize>> = match canonical.as_str()
192 {
193 "LET" => Box::new((0..args.len().saturating_sub(1)).step_by(2)),
194 "LAMBDA" => Box::new(0..args.len().saturating_sub(1)),
195 _ => Box::new(std::iter::empty()),
196 };
197 for index in declaration_indices {
198 if let Some(ASTNode {
199 node_type:
200 ASTNodeType::Reference {
201 reference: ReferenceType::NamedRange(name),
202 ..
203 },
204 ..
205 }) = args.get(index)
206 {
207 out.insert(name.to_ascii_uppercase());
208 }
209 }
210 for arg in args {
211 local_binding_declarations(arg, out);
212 }
213 }
214 ASTNodeType::UnaryOp { expr, .. } => local_binding_declarations(expr, out),
215 ASTNodeType::BinaryOp { left, right, .. } => {
216 local_binding_declarations(left, out);
217 local_binding_declarations(right, out);
218 }
219 ASTNodeType::Call { callee, args } => {
220 local_binding_declarations(callee, out);
221 for arg in args {
222 local_binding_declarations(arg, out);
223 }
224 }
225 ASTNodeType::Array(rows) => {
226 for item in rows.iter().flatten() {
227 local_binding_declarations(item, out);
228 }
229 }
230 _ => {}
231 }
232 }
233
234 pub fn assert_case<R: EvaluationContext>(
235 engine: &mut Engine<R>,
236 formula: &str,
237 placement: CellRef,
238 ) -> Fp8ParityObservation {
239 let parsed = parse_formula(formula);
240 assert_case_ast(engine, formula, parsed, placement)
241 }
242
243 pub fn assert_case_ast<R: EvaluationContext>(
244 engine: &mut Engine<R>,
245 formula: &str,
246 parsed: ASTNode,
247 placement: CellRef,
248 ) -> Fp8ParityObservation {
249 let mut local_declarations = BTreeSet::new();
250 local_binding_declarations(&parsed, &mut local_declarations);
251 let mut old_ast = parsed.clone();
252 let old_rewrite = engine
253 .graph
254 .rewrite_structured_references_for_cell(&mut old_ast, placement);
255 let old = old_rewrite.and_then(|_| old_path(engine, &old_ast, placement));
256
257 let new = {
258 let mut pipeline = engine.ingest_pipeline();
259 pipeline.ingest_formula(
260 crate::engine::ingest_pipeline::FormulaAstInput::Tree(parsed),
261 placement,
262 Some(Arc::<str>::from(formula)),
263 )
264 };
265
266 match (old, new) {
267 (Ok(old), Ok(new)) => {
268 let new_direct = sorted_cells(new.dep_plan.direct_cell_deps.clone());
269 assert_eq!(
270 old.direct_cells, new_direct,
271 "direct deps differ for {formula} at {placement:?}\nold={:?}\nnew={:?}",
272 old.direct_cells, new_direct
273 );
274 assert_eq!(
275 old.range_deps, new.dep_plan.range_deps,
276 "range deps differ for {formula} at {placement:?}"
277 );
278 let old_unresolved_names: Vec<_> = old
279 .unresolved_names
280 .iter()
281 .filter(|name| !local_declarations.contains(&name.to_ascii_uppercase()))
282 .cloned()
283 .collect();
284 assert_eq!(
285 old_unresolved_names, new.dep_plan.named_refs,
286 "unresolved names differ for {formula} at {placement:?}"
287 );
288 assert_eq!(
289 old.volatile, new.dep_plan.volatile,
290 "volatile flag differs for {formula} at {placement:?}"
291 );
292 assert_eq!(
293 old.dynamic, new.dep_plan.dynamic,
294 "dynamic flag differs for {formula} at {placement:?}"
295 );
296 let mut expected_labels = canonical_labels_from_old(&old.labels);
297 if old.dynamic {
298 expected_labels.flags |= CanonicalLabels::FLAG_DYNAMIC;
299 }
300 assert_eq!(
301 expected_labels.flags, new.labels.flags,
302 "canonical label flags differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
303 old.labels.flags, new.labels.flags
304 );
305 assert_eq!(
306 expected_labels.rejects, new.labels.rejects,
307 "canonical label rejects differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
308 old.labels.reject_reasons, new.labels.rejects
309 );
310 let named_resolution_superset = old.summary_rejected_only_for_named_reference
316 && old.read_summary_debug.is_none();
317 if !named_resolution_superset {
318 assert_eq!(
319 old.read_summary_debug,
320 new.read_summary.as_ref().map(|s| format!("{s:?}")),
321 "read summary differs for {formula} at {placement:?}"
322 );
323 }
324 assert_eq!(new.formula_text.as_deref(), Some(formula));
325 assert_eq!(new.placement, placement);
326 Fp8ParityObservation {
327 formula: formula.to_string(),
328 placement,
329 old_payload: old.payload,
330 new_hash: new.canonical_hash,
331 }
332 }
333 (Err(old), Err(new)) => {
334 assert_eq!(
335 old.kind.to_string(),
336 new.kind.to_string(),
337 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
338 );
339 Fp8ParityObservation {
340 formula: formula.to_string(),
341 placement,
342 old_payload: format!("ERR:{:?}", old.kind),
343 new_hash: 0,
344 }
345 }
346 (Ok(_), Err(new)) => panic!(
347 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
348 ),
349 (Err(old), Ok(_)) => panic!(
350 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
351 ),
352 }
353 }
354
355 #[derive(Debug)]
356 struct OldOutput {
357 payload: String,
358 labels: crate::engine::template::canonical::CanonicalTemplateLabels,
359 direct_cells: Vec<CellRef>,
360 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
361 unresolved_names: Vec<String>,
362 volatile: bool,
363 dynamic: bool,
364 read_summary_debug: Option<String>,
365 summary_rejected_only_for_named_reference: bool,
366 }
367
368 fn old_path<R: EvaluationContext>(
369 engine: &mut Engine<R>,
370 ast: &ASTNode,
371 placement: CellRef,
372 ) -> Result<OldOutput, ExcelError> {
373 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
374 .graph
375 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
376 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
377 let dynamic = engine.graph.is_ast_dynamic(ast);
378 let template =
379 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
380 let summary = summarize_canonical_template(&template);
381 let scalar_domain = PlacementDomain::row_run(
382 placement.sheet_id,
383 placement.coord.row(),
384 placement.coord.row(),
385 placement.coord.col(),
386 );
387 let result_region = ResultRegion::scalar_cells(scalar_domain);
388 let read_summary = SpanReadSummary::from_formula_summary(
389 placement.sheet_id,
390 &result_region,
391 &summary,
392 engine.graph.sheet_reg(),
393 )
394 .ok();
395 let summary_rejected_only_for_named_reference = !summary.reject_reasons.is_empty()
396 && summary.reject_reasons.iter().all(|reason| {
397 matches!(
398 reason,
399 crate::engine::template::dependency_summary::DependencyRejectReason
400 ::NamedRangeUnsupported { .. }
401 )
402 });
403 Ok(OldOutput {
404 payload: template.key.payload().to_string(),
405 labels: template.labels,
406 direct_cells: sorted_cells(placeholders),
407 range_deps: ranges,
408 unresolved_names,
409 volatile,
410 dynamic,
411 read_summary_debug: read_summary.as_ref().map(|s| format!("{s:?}")),
412 summary_rejected_only_for_named_reference,
413 })
414 }
415
416 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
417 cells.sort();
418 cells.dedup();
419 cells
420 }
421
422 fn canonical_labels_from_old(
423 old: &crate::engine::template::canonical::CanonicalTemplateLabels,
424 ) -> CanonicalLabels {
425 let mut labels = CanonicalLabels::default();
426 for flag in &old.flags {
427 labels.flags |= match flag {
428 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
429 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
430 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
431 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
432 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
433 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
434 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
435 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
436 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
437 };
438 }
439 for reason in &old.reject_reasons {
440 labels.flags |= match reason {
441 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
442 CanonicalLabels::FLAG_DYNAMIC
443 }
444 CanonicalRejectReason::ParserVolatileFlag
445 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
446 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
447 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
448 }
449 CanonicalRejectReason::ArrayOrSpillFunction { .. }
450 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
451 CanonicalRejectReason::StructuredReference { .. }
452 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
453 CanonicalLabels::FLAG_CONTAINS_TABLE
454 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
455 }
456 CanonicalRejectReason::OpenRangeReference { .. }
457 | CanonicalRejectReason::WholeAxisReference { .. } => {
458 CanonicalLabels::FLAG_CONTAINS_RANGE
459 }
460 _ => 0,
461 };
462 labels.rejects |= match reason {
463 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
464 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
465 }
466 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
467 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
468 }
469 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
470 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
471 }
472 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
473 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
474 }
475 CanonicalRejectReason::ParserVolatileFlag => {
476 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
477 }
478 CanonicalRejectReason::VolatileFunction { .. } => {
479 CanonicalLabels::REJECT_VOLATILE_FUNCTION
480 }
481 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
482 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
483 }
484 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
485 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
486 }
487 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
488 CanonicalRejectReason::SpillReference { .. } => {
489 CanonicalLabels::REJECT_SPILL_REFERENCE
490 }
491 CanonicalRejectReason::SpillResultRegionOperator => {
492 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
493 }
494 CanonicalRejectReason::ImplicitIntersectionOperator => {
495 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
496 }
497 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
498 CanonicalRejectReason::StructuredReference { .. } => {
499 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
500 }
501 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
502 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
503 }
504 CanonicalRejectReason::ThreeDReference { .. } => {
505 CanonicalLabels::REJECT_THREE_D_REFERENCE
506 }
507 CanonicalRejectReason::ExternalReference { .. } => {
508 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
509 }
510 CanonicalRejectReason::OpenRangeReference { .. } => {
511 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
512 }
513 CanonicalRejectReason::WholeAxisReference { .. } => {
514 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
515 }
516 CanonicalRejectReason::UnsupportedReference { .. } => {
517 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
518 }
519 CanonicalRejectReason::FunctionContractUnsupported { .. }
520 | CanonicalRejectReason::ContextDependentFunction { .. } => {
521 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
522 }
523 };
524 }
525 labels
526 }
527
528 pub fn literal_number(value: f64) -> LiteralValue {
529 LiteralValue::Number(value)
530 }
531}
532
533use crate::timezone::TimeZoneSpec;
536use crate::traits::EvaluationContext;
537use crate::traits::VolatileLevel;
538use chrono::{DateTime, Utc};
539use formualizer_common::error::{ExcelError, ExcelErrorKind};
540use std::collections::HashMap;
541
542impl<R: EvaluationContext> Engine<R> {
543 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
544 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
545 }
546
547 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
548 self.graph.store_ast(ast)
549 }
550
551 pub fn stage_formula_ast(
560 &mut self,
561 grouper: &mut FormulaFamilyGrouper,
562 row: u32,
563 col: u32,
564 ast: &formualizer_parse::parser::ASTNode,
565 formula_text: Option<std::sync::Arc<str>>,
566 ) -> FormulaIngestRecord {
567 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
568 if self.config.formula_compression
569 && let Some(family) = self.graph.group_formula_member(grouper, row0, col0, ast)
570 {
571 let record = FormulaIngestRecord::member(row, col, family.template, family.anchor);
572 grouper.members += 1;
573 grouper.note(row0, col0, family);
574 return record;
575 }
576 let ast_id = self.intern_formula_ast(ast);
577 self.note_staged_formula(grouper, row, col, ast_id);
578 FormulaIngestRecord::new(row, col, ast_id, formula_text)
579 }
580
581 pub fn note_staged_formula(
584 &mut self,
585 grouper: &mut FormulaFamilyGrouper,
586 row: u32,
587 col: u32,
588 ast_id: AstNodeId,
589 ) {
590 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
591 grouper.note(
592 row0,
593 col0,
594 formula_ingest::GroupedFamily {
595 template: ast_id,
596 anchor: (row0, col0),
597 rendered: None,
598 },
599 );
600 }
601}
602
603pub trait CalcObserver: Send + Sync {
605 fn on_eval_start(&self, vertex_id: VertexId);
606 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
607 fn on_cycle_detected(&self, cycle: &[VertexId]);
608 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
609}
610
611impl CalcObserver for () {
613 fn on_eval_start(&self, _vertex_id: VertexId) {}
614 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
615 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
616 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
617}
618
619#[derive(Debug, Clone, PartialEq, Eq)]
623pub enum DeterministicMode {
624 Disabled {
626 timezone: TimeZoneSpec,
628 },
629 Enabled {
631 timestamp_utc: DateTime<Utc>,
633 timezone: TimeZoneSpec,
635 },
636}
637
638impl Default for DeterministicMode {
639 fn default() -> Self {
640 Self::Disabled {
641 timezone: TimeZoneSpec::default(),
642 }
643 }
644}
645
646impl DeterministicMode {
647 pub fn is_enabled(&self) -> bool {
648 matches!(self, DeterministicMode::Enabled { .. })
649 }
650
651 pub fn timezone(&self) -> &TimeZoneSpec {
652 match self {
653 DeterministicMode::Disabled { timezone } => timezone,
654 DeterministicMode::Enabled { timezone, .. } => timezone,
655 }
656 }
657
658 pub fn validate(&self) -> Result<(), ExcelError> {
659 if let DeterministicMode::Enabled { timezone, .. } = self {
660 timezone
661 .validate_for_determinism()
662 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
663 }
664 Ok(())
665 }
666
667 pub fn build_clock(
668 &self,
669 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
670 self.validate()?;
671 Ok(match self {
672 #[cfg(feature = "system-clock")]
673 DeterministicMode::Disabled { timezone } => {
674 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
675 }
676 #[cfg(not(feature = "system-clock"))]
677 DeterministicMode::Disabled { timezone: _ } => {
678 std::sync::Arc::new(crate::timezone::FixedClock::new(
683 chrono::DateTime::UNIX_EPOCH,
684 crate::timezone::TimeZoneSpec::Utc,
685 ))
686 }
687 DeterministicMode::Enabled {
688 timestamp_utc,
689 timezone,
690 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
691 *timestamp_utc,
692 timezone.clone(),
693 )),
694 })
695 }
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq)]
700pub enum FormulaParsePolicy {
701 Strict,
703 CoerceToError,
705 KeepCachedValue,
707 AsText,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq)]
713pub struct FormulaParseDiagnostic {
714 pub sheet: String,
715 pub row: u32,
716 pub col: u32,
717 pub formula: String,
718 pub message: String,
719 pub policy: FormulaParsePolicy,
720}
721
722#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
723pub enum FormulaPlaneMode {
724 #[default]
726 Off,
727 Shadow,
729 AuthoritativeExperimental,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum FormulaSpoolDiskPolicy {
736 NativeSpill,
738 MemoryOnly,
740}
741
742#[derive(Debug, Clone, PartialEq, Eq)]
744pub struct WorkbookLoadLimits {
745 pub max_sheet_rows: u32,
747 pub max_sheet_cols: u32,
749 pub max_sheet_logical_cells: u64,
751 pub max_formula_plane_fallback_cells: u64,
754 pub sparse_sheet_cell_threshold: u64,
756 pub max_sparse_cell_ratio: u64,
758 pub max_formula_spool_bytes_per_sheet: u64,
760 pub max_formula_spool_bytes_per_workbook: u64,
762 pub max_formula_spool_files_per_workbook: u32,
764 pub formula_spool_memory_prefix_bytes: u64,
766 pub max_formula_spool_memory_bytes: u64,
768 pub formula_spool_disk_policy: FormulaSpoolDiskPolicy,
770}
771
772impl Default for WorkbookLoadLimits {
773 fn default() -> Self {
774 Self {
775 max_sheet_rows: 1_048_576,
776 max_sheet_cols: 16_384,
777 max_sheet_logical_cells: 128_000_000,
778 max_formula_plane_fallback_cells: 2_000_000,
779 sparse_sheet_cell_threshold: 250_000,
780 max_sparse_cell_ratio: 1_024,
781 max_formula_spool_bytes_per_sheet: 256 * 1024 * 1024,
782 max_formula_spool_bytes_per_workbook: 1024 * 1024 * 1024,
783 max_formula_spool_files_per_workbook: 1_024,
784 formula_spool_memory_prefix_bytes: 1024 * 1024,
785 max_formula_spool_memory_bytes: 16 * 1024 * 1024,
786 formula_spool_disk_policy: if cfg!(target_arch = "wasm32") {
787 FormulaSpoolDiskPolicy::MemoryOnly
788 } else {
789 FormulaSpoolDiskPolicy::NativeSpill
790 },
791 }
792 }
793}
794
795#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
797pub enum TemporalEgress {
798 #[default]
799 Native,
800 Serial,
801}
802
803#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
806pub enum PreparationPolicy {
807 Strict,
810 #[default]
814 BestEffort,
815}
816
817#[derive(Debug, Clone)]
819pub struct EvalConfig {
820 pub enable_parallel: bool,
821 pub max_threads: Option<usize>,
822 pub max_vertices: Option<usize>,
825 pub max_eval_time: Option<std::time::Duration>,
828 pub max_memory_mb: Option<usize>,
832 pub evaluation_budgets: EvaluationBudgets,
836
837 pub default_sheet_name: String,
839
840 pub case_sensitive_names: bool,
844
845 pub case_sensitive_tables: bool,
849
850 pub workbook_seed: u64,
852
853 pub volatile_level: VolatileLevel,
855
856 pub deterministic_mode: DeterministicMode,
858
859 pub range_expansion_limit: usize,
862
863 pub max_open_ended_rows: u32,
867
868 pub max_open_ended_cols: u32,
872
873 pub stripe_height: u32,
875 pub stripe_width: u32,
877 pub enable_block_stripes: bool,
881
882 pub spill: SpillConfig,
884
885 pub cycle: CycleConfig,
889
890 pub use_dynamic_topo: bool,
895 pub pk_visit_budget: usize,
897 pub pk_compaction_interval_ops: u64,
899 pub max_layer_width: Option<usize>,
901 pub pk_reject_cycle_edges: bool,
904 pub sheet_index_mode: SheetIndexMode,
906
907 pub warmup: tuning::WarmupConfig,
909
910 pub arrow_storage_enabled: bool,
912 pub delta_overlay_enabled: bool,
914
915 pub write_formula_overlay_enabled: bool,
918
919 pub max_overlay_memory_bytes: Option<usize>,
924
925 pub date_system: DateSystem,
927
928 pub temporal_egress: TemporalEgress,
930
931 pub formula_parse_policy: FormulaParsePolicy,
933
934 pub defer_graph_building: bool,
937
938 pub preparation_policy: PreparationPolicy,
941
942 pub enable_virtual_dep_telemetry: bool,
946
947 pub formula_plane_mode: FormulaPlaneMode,
951 pub max_formula_plane_cache_candidates: usize,
953 pub max_formula_plane_cache_edges: usize,
955 pub max_formula_plane_cache_bytes: usize,
957
958 pub lookup_index_cache_max_bytes: usize,
960
961 pub family_execution: bool,
966
967 pub family_kernels: bool,
971
972 pub family_lift: bool,
977
978 pub formula_compression: bool,
984}
985
986impl Default for EvalConfig {
987 fn default() -> Self {
988 Self {
989 enable_parallel: true,
990 max_threads: None,
991 max_vertices: None,
992 max_eval_time: None,
993 max_memory_mb: None,
994 evaluation_budgets: EvaluationBudgets::default(),
995
996 default_sheet_name: format!("Sheet{}", 1),
997
998 case_sensitive_names: false,
1000 case_sensitive_tables: false,
1001
1002 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
1004
1005 volatile_level: VolatileLevel::Always,
1007
1008 deterministic_mode: DeterministicMode::default(),
1009
1010 range_expansion_limit: 64,
1012 max_open_ended_rows: 1_048_576,
1015 max_open_ended_cols: 16_384,
1016 stripe_height: 256,
1017 stripe_width: 256,
1018 enable_block_stripes: false,
1019 spill: SpillConfig::default(),
1020 cycle: CycleConfig::default(),
1021
1022 use_dynamic_topo: false, pk_visit_budget: 50_000,
1025 pk_compaction_interval_ops: 100_000,
1026 max_layer_width: None,
1027 pk_reject_cycle_edges: false,
1028 sheet_index_mode: SheetIndexMode::Eager,
1029 warmup: tuning::WarmupConfig::default(),
1030 arrow_storage_enabled: true,
1031 delta_overlay_enabled: true,
1032 write_formula_overlay_enabled: true,
1033 max_overlay_memory_bytes: None,
1034 date_system: DateSystem::Excel1900,
1035 temporal_egress: TemporalEgress::default(),
1036 formula_parse_policy: FormulaParsePolicy::Strict,
1037 defer_graph_building: false,
1038 preparation_policy: PreparationPolicy::BestEffort,
1039 enable_virtual_dep_telemetry: false,
1040 formula_plane_mode: FormulaPlaneMode::Off,
1041 max_formula_plane_cache_candidates: 100_000,
1042 max_formula_plane_cache_edges: 100_000,
1043 max_formula_plane_cache_bytes: 64 * 1024 * 1024,
1044 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
1045 family_execution: true,
1046 family_kernels: true,
1047 family_lift: true,
1048 formula_compression: true,
1049 }
1050 }
1051}
1052
1053impl EvalConfig {
1054 #[inline]
1055 pub fn with_preparation_policy(mut self, policy: PreparationPolicy) -> Self {
1056 self.preparation_policy = policy;
1057 self
1058 }
1059
1060 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
1061 self.range_expansion_limit = limit;
1062 self
1063 }
1064
1065 #[inline]
1066 pub fn with_parallel(mut self, enable: bool) -> Self {
1067 self.enable_parallel = enable;
1068 self
1069 }
1070
1071 #[inline]
1072 pub fn with_block_stripes(mut self, enable: bool) -> Self {
1073 self.enable_block_stripes = enable;
1074 self
1075 }
1076
1077 #[inline]
1078 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
1079 self.case_sensitive_names = enable;
1080 self
1081 }
1082
1083 #[inline]
1084 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
1085 self.case_sensitive_tables = enable;
1086 self
1087 }
1088
1089 #[inline]
1090 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
1091 self.arrow_storage_enabled = enable;
1092 self
1093 }
1094
1095 #[inline]
1096 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
1097 self.delta_overlay_enabled = enable;
1098 self
1099 }
1100
1101 #[inline]
1102 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
1103 self.write_formula_overlay_enabled = enable;
1104 self
1105 }
1106
1107 #[inline]
1108 pub fn with_date_system(mut self, system: DateSystem) -> Self {
1109 self.date_system = system;
1110 self
1111 }
1112
1113 #[inline]
1114 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
1115 self.formula_parse_policy = policy;
1116 self
1117 }
1118
1119 #[inline]
1120 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
1121 self.enable_virtual_dep_telemetry = enable;
1122 self
1123 }
1124
1125 #[inline]
1126 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
1127 self.formula_plane_mode = mode;
1128 self
1129 }
1130
1131 #[inline]
1132 pub fn with_evaluation_budgets(mut self, budgets: EvaluationBudgets) -> Self {
1133 self.evaluation_budgets = budgets;
1134 self
1135 }
1136
1137 pub fn resolved_evaluation_budgets(&self) -> EvaluationBudgets {
1139 resource_ledger::resolve_evaluation_budgets(
1140 &self.evaluation_budgets,
1141 self.max_vertices,
1142 self.max_memory_mb,
1143 self.max_eval_time,
1144 )
1145 .budgets
1146 }
1147
1148 #[inline]
1157 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
1158 if let Err(msg) = cycle.validate() {
1159 panic!("invalid CycleConfig: {msg}");
1160 }
1161 self.cycle = cycle;
1162 self
1163 }
1164}
1165
1166#[derive(Debug, Clone, Copy, PartialEq, Default)]
1171pub struct CycleConfig {
1172 pub detection: CycleDetection,
1173 pub policy: CyclePolicy,
1174}
1175
1176impl CycleConfig {
1177 pub fn iterate_excel_defaults() -> Self {
1180 Self {
1181 detection: CycleDetection::Runtime,
1182 policy: CyclePolicy::iterate_excel_defaults(),
1183 }
1184 }
1185
1186 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
1188 Self {
1189 detection: CycleDetection::Runtime,
1190 policy: CyclePolicy::Iterate {
1191 max_iterations,
1192 max_change,
1193 },
1194 }
1195 }
1196
1197 pub fn validate(&self) -> Result<(), String> {
1201 if let CyclePolicy::Iterate {
1202 max_iterations,
1203 max_change,
1204 } = self.policy
1205 {
1206 if self.detection == CycleDetection::Static {
1207 return Err(
1208 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
1209 );
1210 }
1211 if max_iterations == 0 {
1212 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
1213 }
1214 if !max_change.is_finite() || max_change < 0.0 {
1215 return Err(format!(
1216 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
1217 ));
1218 }
1219 }
1220 Ok(())
1221 }
1222
1223 #[inline]
1228 pub(crate) fn allows_self_dependency(&self) -> bool {
1229 self.detection == CycleDetection::Runtime
1230 && matches!(self.policy, CyclePolicy::Iterate { .. })
1231 }
1232}
1233
1234#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1236pub enum CycleDetection {
1237 #[default]
1240 Static,
1241 Runtime,
1245}
1246
1247#[derive(Debug, Clone, Copy, PartialEq, Default)]
1249pub enum CyclePolicy {
1250 #[default]
1252 Error,
1253 Iterate {
1261 max_iterations: u32,
1265 max_change: f64,
1269 },
1270}
1271
1272impl CyclePolicy {
1273 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1275 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1277
1278 pub fn iterate_excel_defaults() -> Self {
1280 CyclePolicy::Iterate {
1281 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
1282 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
1283 }
1284 }
1285}
1286
1287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1288pub enum SheetIndexMode {
1289 Eager,
1291 Lazy,
1293 FastBatch,
1295}
1296
1297pub use formualizer_common::DateSystem;
1298
1299pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1301where
1302 R: EvaluationContext + 'static,
1303{
1304 Engine::new(resolver, config)
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1309pub struct SpillConfig {
1310 pub conflict_policy: SpillConflictPolicy,
1312 pub tiebreaker: SpillTiebreaker,
1314 pub bounds_policy: SpillBoundsPolicy,
1316 pub buffer_mode: SpillBufferMode,
1318 pub memory_budget_bytes: Option<u64>,
1320 pub cancellation: SpillCancellationPolicy,
1322 pub visibility: SpillVisibility,
1324
1325 pub max_spill_cells: u32,
1329}
1330
1331impl Default for SpillConfig {
1332 fn default() -> Self {
1333 Self {
1334 conflict_policy: SpillConflictPolicy::Error,
1335 tiebreaker: SpillTiebreaker::FirstWins,
1336 bounds_policy: SpillBoundsPolicy::Strict,
1337 buffer_mode: SpillBufferMode::ShadowBuffer,
1338 memory_budget_bytes: None,
1339 cancellation: SpillCancellationPolicy::Cooperative,
1340 visibility: SpillVisibility::OnCommit,
1341 max_spill_cells: 10_000,
1343 }
1344 }
1345}
1346
1347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1348pub enum SpillConflictPolicy {
1349 Error,
1350 Preempt,
1351}
1352
1353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1354pub enum SpillTiebreaker {
1355 FirstWins,
1356 EvaluationEpochAsc,
1357 AnchorAddressAsc,
1358 FunctionPriorityThenAddress,
1359}
1360
1361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1362pub enum SpillBoundsPolicy {
1363 Strict,
1364 Truncate,
1365}
1366
1367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1368pub enum SpillBufferMode {
1369 ShadowBuffer,
1370 PersistenceJournal,
1371}
1372
1373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1374pub enum SpillCancellationPolicy {
1375 Cooperative,
1376 Strict,
1377}
1378
1379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1380pub enum SpillVisibility {
1381 OnCommit,
1382 StagedLayer,
1383}
1384
1385#[derive(Debug, Default)]
1396pub struct TombstoneRegistry {
1397 pub pending_references: HashMap<String, Vec<VertexId>>,
1399}
1400
1401impl TombstoneRegistry {
1402 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1404 self.pending_references
1405 .entry(sheet_name)
1406 .or_default()
1407 .push(vertex_id);
1408 }
1409
1410 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1412 self.pending_references
1413 .remove(sheet_name)
1414 .unwrap_or_default()
1415 }
1416}