1pub mod addr;
6#[doc(hidden)]
8pub mod affected_closure;
9pub mod arrow_ingest;
10#[doc(hidden)]
13pub mod authority;
14pub mod cancel;
15pub mod resumable;
16pub use resumable::{
17 EvaluationProgress, EvaluationStop, IncompleteEvaluation, RecalcJobError, RecalcJobOptions,
18 RecalcOutcome, SlowUnitKind, SlowUnitReport, StepDelta, StepPhase, StepProgress, StepReport,
19 StopReason,
20};
21pub(crate) mod convergence;
22pub(crate) mod derived_formats;
23pub mod effects;
24pub mod eval;
25pub mod eval_delta;
26pub mod formula_ingest;
27mod formula_source;
28pub mod graph;
29pub(crate) mod idset;
30pub mod ingest;
31pub mod ingest_builder;
32pub(crate) mod ingest_pipeline;
33pub mod inspect;
34pub mod journal;
35pub mod live_edges;
36pub mod live_graph;
37pub mod lookup_index_cache;
38pub mod plan;
39#[cfg(test)]
40mod plan_legacy_tests;
41mod poison;
42pub mod range_view;
43pub(crate) mod refs;
44pub mod resource_ledger;
45pub mod resource_observability;
46pub(crate) mod result_finalization;
47pub mod row_visibility;
48pub mod scheduler;
49pub(crate) mod shape_memo;
50mod source_family;
51pub mod spill;
52mod target_preparation;
53#[doc(hidden)]
54pub mod template;
55pub(crate) mod used_extent;
56pub mod vertex;
57pub mod virtual_deps;
58
59#[cfg(any(test, feature = "legacy_oracle"))]
63pub mod csr_edges;
64pub mod debug_views;
65#[cfg(any(test, feature = "legacy_oracle"))]
68pub mod delta_edges;
69pub mod interval_tree;
70pub mod named_range;
71pub mod sheet_index;
72pub mod sheet_registry;
73#[cfg(any(test, feature = "legacy_oracle"))]
76pub mod topo;
77pub(crate) mod trace;
78pub mod vertex_store;
79
80pub mod arena;
82
83pub mod tuning;
85
86#[cfg(test)]
87mod tests;
88
89pub use arena::AstNodeId;
90pub use cancel::CancelToken;
91pub use eval::{
92 CycleTelemetry, Engine, EngineAction, EngineBaselineStats, EvalResult, RecalcPlan,
93 SourceFormulaIngress, TableMetadata, VirtualDepTelemetry,
94};
95pub use eval_delta::{
96 DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy, EvalDeltaRecord, TARGET_EVAL_DELTA_VERSION,
97 TargetEvalDelta,
98};
99pub use formula_ingest::{
100 FormulaFamilyGrouper, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
101};
102#[doc(hidden)]
103pub use formula_source::{
104 DeferredFormulaPackage, DeferredFormulaReplay, DeferredReplayFormula,
105 ExplicitPartitionLegacyMembers, ExplicitSourceFamilyMembers, FormulaCompressedPreparation,
106 FormulaCompressedSourceBatch, FormulaCompressedSourceReport,
107 FormulaReplayCoordinateDisposition, FormulaReplayDisposition, FormulaReplayPartitionRouter,
108 MAX_EXPLICIT_SOURCE_FAMILY_MEMBERS, MAX_PARTITIONED_SOURCE_FAMILY_FRAGMENTS,
109 PartitionLegacyMember, PartitionLegacyMemberKind, PartitionReconciliation,
110 PartitionedSourceFormulaFamily, PlacementDomainTransport, SourceCoord, SourceFamilyId,
111 SourceFamilyMembers, SourceFormulaFamily, SourceFormulaOrder, SourceRect,
112};
113pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
114#[doc(hidden)]
115pub use source_family::{SourceFamilyCounters, SourceFamilyMode, source_family_process_totals};
116#[allow(deprecated)]
117pub use target_preparation::PrepareTargetsOptions;
118pub use formualizer_common::{ResourceExhaustionDetail, ResourceExhaustionReason};
120pub use graph::FormulaView;
121pub use graph::snapshot::VertexSnapshot;
122pub use graph::{
123 ChangeEvent, DependencyGraph, DependencyRef, GraphBaselineStats, OperationSummary, StripeKey,
124 StripeType, block_index,
125};
126pub use resource_ledger::{
127 AdmissionResourceBudget, DeadlineResourceBudget, DiskScratchPolicy, EvaluationBudgets,
128 EvaluationIncompleteReason, EvaluationResourceConfigDiagnostic,
129 LegacyResourceConfigDisposition, OptimizationResourceBudget, ResourceEnvelope,
130 ResourceLedgerError, ResourceLedgerSnapshot, RetainedResourceBudget, ScratchResourceBudget,
131 SemanticResourceBudget, WorkResourceBudget,
132};
133pub(crate) use resource_ledger::ResourceLedger;
136pub use resource_observability::{
137 EvaluationRequestKind, EvaluationRequestOutcome, EvaluationRequestPhaseTimings,
138 EvaluationResourceBaselineStats, EvaluationResourceClass, EvaluationResourceLedgerRequestStats,
139 EvaluationResourceReason, EvaluationResourceRequestStats, FormulaDirtyLeaseOutcome,
140 FormulaPlaneRoute, FormulaPlaneRouteEvent, FormulaPlaneRoutePhase,
141 FormulaPlaneRouteTransitionReason, FormulaPlaneTopologyCacheOutcome,
142 FormulaPlaneTopologyRequestStats, FormulaPlaneTopologyStrategy,
143};
144pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
145#[cfg(any(test, feature = "legacy_oracle"))]
148pub use scheduler::Scheduler;
149pub use scheduler::{Layer, Schedule, ScheduleUnit};
150pub use target_preparation::{
151 EvaluationTarget, OpaquePreparePolicy, OpaqueReason, PreparationOutcome, PreparationRevision,
152 PrepareScope, PreparedTargetGraphReport, RequestId, TableSelection, TargetEvalOptions,
153};
154pub use vertex::{VertexId, VertexKind};
155
156pub use graph::editor::{
157 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
158 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
159};
160
161pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
162
163#[doc(hidden)]
164pub mod fp8_parity_test_support {
165 use super::{Engine, EvalConfig};
166 use crate::engine::arena::CanonicalLabels;
167 use crate::engine::template::canonical::{
168 CanonicalRejectReason, CanonicalTemplateFlag, canonicalize_template,
169 };
170 use crate::reference::{CellRef, Coord};
171 use crate::traits::EvaluationContext;
172 use formualizer_common::{ExcelError, LiteralValue};
173 use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, parse};
174 use std::collections::BTreeSet;
175 use std::sync::Arc;
176
177 #[derive(Clone, Debug)]
178 pub struct Fp8ParityObservation {
179 pub formula: String,
180 pub placement: CellRef,
181 pub old_payload: String,
182 pub new_hash: u64,
183 }
184
185 pub fn default_config() -> EvalConfig {
186 EvalConfig::default()
187 }
188
189 pub fn parse_formula(formula: &str) -> ASTNode {
190 parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"))
191 }
192
193 pub fn cell(sheet_id: u16, row: u32, col: u32) -> CellRef {
194 CellRef::new(sheet_id, Coord::from_excel(row, col, true, true))
195 }
196
197 fn local_binding_declarations(ast: &ASTNode, out: &mut BTreeSet<String>) {
198 match &ast.node_type {
199 ASTNodeType::Function { name, args } => {
200 let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
201 let declaration_indices: Box<dyn Iterator<Item = usize>> = match canonical.as_str()
202 {
203 "LET" => Box::new((0..args.len().saturating_sub(1)).step_by(2)),
204 "LAMBDA" => Box::new(0..args.len().saturating_sub(1)),
205 _ => Box::new(std::iter::empty()),
206 };
207 for index in declaration_indices {
208 if let Some(ASTNode {
209 node_type:
210 ASTNodeType::Reference {
211 reference: ReferenceType::NamedRange(name),
212 ..
213 },
214 ..
215 }) = args.get(index)
216 {
217 out.insert(name.to_ascii_uppercase());
218 }
219 }
220 for arg in args {
221 local_binding_declarations(arg, out);
222 }
223 }
224 ASTNodeType::UnaryOp { expr, .. } => local_binding_declarations(expr, out),
225 ASTNodeType::BinaryOp { left, right, .. } => {
226 local_binding_declarations(left, out);
227 local_binding_declarations(right, out);
228 }
229 ASTNodeType::Call { callee, args } => {
230 local_binding_declarations(callee, out);
231 for arg in args {
232 local_binding_declarations(arg, out);
233 }
234 }
235 ASTNodeType::Array(rows) => {
236 for item in rows.iter().flatten() {
237 local_binding_declarations(item, out);
238 }
239 }
240 _ => {}
241 }
242 }
243
244 pub fn assert_case<R: EvaluationContext>(
245 engine: &mut Engine<R>,
246 formula: &str,
247 placement: CellRef,
248 ) -> Fp8ParityObservation {
249 let parsed = parse_formula(formula);
250 assert_case_ast(engine, formula, parsed, placement)
251 }
252
253 pub fn assert_case_ast<R: EvaluationContext>(
254 engine: &mut Engine<R>,
255 formula: &str,
256 parsed: ASTNode,
257 placement: CellRef,
258 ) -> Fp8ParityObservation {
259 let mut local_declarations = BTreeSet::new();
260 local_binding_declarations(&parsed, &mut local_declarations);
261 let mut old_ast = parsed.clone();
262 let old_rewrite = engine
263 .graph
264 .rewrite_structured_references_for_cell(&mut old_ast, placement);
265 let old = old_rewrite.and_then(|_| old_path(engine, &old_ast, placement));
266
267 let new = {
268 let mut pipeline = engine.ingest_pipeline();
269 pipeline.ingest_formula(
270 crate::engine::ingest_pipeline::FormulaAstInput::Tree(parsed),
271 placement,
272 Some(Arc::<str>::from(formula)),
273 )
274 };
275
276 match (old, new) {
277 (Ok(old), Ok(new)) => {
278 let new_direct = sorted_cells(new.dep_plan.direct_cell_deps.clone());
279 assert_eq!(
280 old.direct_cells, new_direct,
281 "direct deps differ for {formula} at {placement:?}\nold={:?}\nnew={:?}",
282 old.direct_cells, new_direct
283 );
284 assert_eq!(
285 old.range_deps, new.dep_plan.range_deps,
286 "range deps differ for {formula} at {placement:?}"
287 );
288 let old_unresolved_names: Vec<_> = old
289 .unresolved_names
290 .iter()
291 .filter(|name| !local_declarations.contains(&name.to_ascii_uppercase()))
292 .cloned()
293 .collect();
294 assert_eq!(
295 old_unresolved_names, new.dep_plan.named_refs,
296 "unresolved names differ for {formula} at {placement:?}"
297 );
298 assert_eq!(
299 old.volatile, new.dep_plan.volatile,
300 "volatile flag differs for {formula} at {placement:?}"
301 );
302 assert_eq!(
303 old.dynamic, new.dep_plan.dynamic,
304 "dynamic flag differs for {formula} at {placement:?}"
305 );
306 Fp8ParityObservation {
307 formula: formula.to_string(),
308 placement,
309 old_payload: old.payload,
310 new_hash: old.canonical_hash,
311 }
312 }
313 (Err(old), Err(new)) => {
314 assert_eq!(
315 old.kind.to_string(),
316 new.kind.to_string(),
317 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
318 );
319 Fp8ParityObservation {
320 formula: formula.to_string(),
321 placement,
322 old_payload: format!("ERR:{:?}", old.kind),
323 new_hash: 0,
324 }
325 }
326 (Ok(_), Err(new)) => panic!(
327 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
328 ),
329 (Err(old), Ok(_)) => panic!(
330 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
331 ),
332 }
333 }
334
335 #[derive(Debug)]
336 struct OldOutput {
337 payload: String,
338 direct_cells: Vec<CellRef>,
339 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
340 unresolved_names: Vec<String>,
341 volatile: bool,
342 dynamic: bool,
343 canonical_hash: u64,
344 }
345
346 fn old_path<R: EvaluationContext>(
347 engine: &mut Engine<R>,
348 ast: &ASTNode,
349 placement: CellRef,
350 ) -> Result<OldOutput, ExcelError> {
351 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
352 .graph
353 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
354 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
355 let dynamic = engine.graph.is_ast_dynamic(ast);
356 let template =
357 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
358 engine.intern_formula_ast(ast);
361 let metadata = engine.ingest_pipeline().parity_metadata(ast, placement);
362 let mut expected = canonical_labels_from_old(&template.labels);
363 if dynamic {
364 expected.flags |= CanonicalLabels::FLAG_DYNAMIC;
365 }
366 assert_eq!(
367 expected.flags, metadata.labels.flags,
368 "canonical flags for {ast:?}"
369 );
370 assert_eq!(
371 expected.rejects, metadata.labels.rejects,
372 "canonical rejects for {ast:?}"
373 );
374 Ok(OldOutput {
375 payload: template.key.payload().to_string(),
376 direct_cells: sorted_cells(placeholders),
377 range_deps: ranges,
378 unresolved_names,
379 volatile,
380 dynamic,
381 canonical_hash: metadata.canonical_hash,
382 })
383 }
384
385 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
386 cells.sort();
387 cells.dedup();
388 cells
389 }
390
391 fn canonical_labels_from_old(
392 old: &crate::engine::template::canonical::CanonicalTemplateLabels,
393 ) -> CanonicalLabels {
394 let mut labels = CanonicalLabels::default();
395 for flag in &old.flags {
396 labels.flags |= match flag {
397 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
398 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
399 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
400 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
401 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
402 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
403 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
404 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
405 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
406 };
407 }
408 for reason in &old.reject_reasons {
409 labels.flags |= match reason {
410 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
411 CanonicalLabels::FLAG_DYNAMIC
412 }
413 CanonicalRejectReason::ParserVolatileFlag
414 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
415 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
416 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
417 }
418 CanonicalRejectReason::ArrayOrSpillFunction { .. }
419 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
420 CanonicalRejectReason::StructuredReference { .. }
421 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
422 CanonicalLabels::FLAG_CONTAINS_TABLE
423 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
424 }
425 CanonicalRejectReason::OpenRangeReference { .. }
426 | CanonicalRejectReason::WholeAxisReference { .. } => {
427 CanonicalLabels::FLAG_CONTAINS_RANGE
428 }
429 _ => 0,
430 };
431 labels.rejects |= match reason {
432 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
433 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
434 }
435 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
436 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
437 }
438 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
439 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
440 }
441 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
442 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
443 }
444 CanonicalRejectReason::ParserVolatileFlag => {
445 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
446 }
447 CanonicalRejectReason::VolatileFunction { .. } => {
448 CanonicalLabels::REJECT_VOLATILE_FUNCTION
449 }
450 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
451 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
452 }
453 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
454 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
455 }
456 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
457 CanonicalRejectReason::SpillReference { .. } => {
458 CanonicalLabels::REJECT_SPILL_REFERENCE
459 }
460 CanonicalRejectReason::SpillResultRegionOperator => {
461 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
462 }
463 CanonicalRejectReason::ImplicitIntersectionOperator => {
464 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
465 }
466 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
467 CanonicalRejectReason::StructuredReference { .. } => {
468 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
469 }
470 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
471 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
472 }
473 CanonicalRejectReason::ThreeDReference { .. } => {
474 CanonicalLabels::REJECT_THREE_D_REFERENCE
475 }
476 CanonicalRejectReason::ExternalReference { .. } => {
477 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
478 }
479 CanonicalRejectReason::OpenRangeReference { .. } => {
480 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
481 }
482 CanonicalRejectReason::WholeAxisReference { .. } => {
483 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
484 }
485 CanonicalRejectReason::UnsupportedReference { .. } => {
486 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
487 }
488 CanonicalRejectReason::FunctionContractUnsupported { .. }
489 | CanonicalRejectReason::ContextDependentFunction { .. } => {
490 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
491 }
492 };
493 }
494 labels
495 }
496
497 pub fn literal_number(value: f64) -> LiteralValue {
498 LiteralValue::Number(value)
499 }
500}
501
502use crate::timezone::TimeZoneSpec;
505use crate::traits::EvaluationContext;
506use crate::traits::VolatileLevel;
507use chrono::{DateTime, Utc};
508use formualizer_common::error::{ExcelError, ExcelErrorKind};
509use std::collections::HashMap;
510
511impl<R: EvaluationContext> Engine<R> {
512 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
516 self.assert_not_poisoned();
517 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
518 }
519
520 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
524 self.assert_not_poisoned();
525 self.graph.store_ast(ast)
526 }
527
528 pub fn stage_formula_ast(
540 &mut self,
541 grouper: &mut FormulaFamilyGrouper,
542 row: u32,
543 col: u32,
544 ast: &formualizer_parse::parser::ASTNode,
545 formula_text: Option<std::sync::Arc<str>>,
546 ) -> FormulaIngestRecord {
547 self.assert_not_poisoned();
548 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
549 if self.config.formula_compression
550 && let Some(family) = self.graph.group_formula_member(grouper, row0, col0, ast)
551 {
552 let record = FormulaIngestRecord::member(row, col, family.template, family.anchor);
553 grouper.members += 1;
554 grouper.counters.parsed_members += 1;
555 grouper.note(row0, col0, family);
556 return record;
557 }
558 let ast_id = self.intern_formula_ast(ast);
559 self.note_staged_formula(grouper, row, col, ast_id);
560 FormulaIngestRecord::new(row, col, ast_id, formula_text)
561 }
562
563 #[doc(hidden)]
566 pub fn source_family_mode(&self) -> SourceFamilyMode {
567 self.source_family_mode
568 }
569
570 #[doc(hidden)]
571 pub fn set_source_family_mode(&mut self, mode: SourceFamilyMode) {
575 self.assert_not_poisoned();
576 self.source_family_mode = mode;
577 }
578
579 #[doc(hidden)]
581 pub fn source_family_counters(&self) -> SourceFamilyCounters {
582 self.source_family_counters
583 }
584
585 #[doc(hidden)]
588 pub fn finish_family_grouper(&mut self, grouper: &mut FormulaFamilyGrouper) {
592 self.assert_not_poisoned();
593 let counters = std::mem::take(&mut grouper.counters);
594 counters.publish();
595 self.source_family_counters.accumulate(&counters);
596 }
597
598 pub fn note_staged_formula(
604 &mut self,
605 grouper: &mut FormulaFamilyGrouper,
606 row: u32,
607 col: u32,
608 ast_id: AstNodeId,
609 ) {
610 self.assert_not_poisoned();
611 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
612 grouper.note(
613 row0,
614 col0,
615 formula_ingest::GroupedFamily {
616 template: ast_id,
617 anchor: (row0, col0),
618 rendered: None,
619 lexical: None,
620 },
621 );
622 }
623}
624
625pub trait CalcObserver: Send + Sync {
630 fn on_eval_start(&self, vertex_id: VertexId);
631 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
632 fn on_cycle_detected(&self, cycle: &[VertexId]);
633 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
634}
635
636impl CalcObserver for () {
638 fn on_eval_start(&self, _vertex_id: VertexId) {}
639 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
640 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
641 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
642}
643
644#[derive(Debug, Clone, PartialEq, Eq)]
648pub enum DeterministicMode {
649 Disabled {
651 timezone: TimeZoneSpec,
653 },
654 Enabled {
656 timestamp_utc: DateTime<Utc>,
658 timezone: TimeZoneSpec,
660 },
661}
662
663impl Default for DeterministicMode {
664 fn default() -> Self {
665 Self::Disabled {
666 timezone: TimeZoneSpec::default(),
667 }
668 }
669}
670
671impl DeterministicMode {
672 pub fn is_enabled(&self) -> bool {
673 matches!(self, DeterministicMode::Enabled { .. })
674 }
675
676 pub fn timezone(&self) -> &TimeZoneSpec {
677 match self {
678 DeterministicMode::Disabled { timezone } => timezone,
679 DeterministicMode::Enabled { timezone, .. } => timezone,
680 }
681 }
682
683 pub fn validate(&self) -> Result<(), ExcelError> {
684 if let DeterministicMode::Enabled { timezone, .. } = self {
685 timezone
686 .validate_for_determinism()
687 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
688 }
689 Ok(())
690 }
691
692 pub fn build_clock(
693 &self,
694 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
695 self.validate()?;
696 Ok(match self {
697 #[cfg(feature = "system-clock")]
698 DeterministicMode::Disabled { timezone } => {
699 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
700 }
701 #[cfg(not(feature = "system-clock"))]
702 DeterministicMode::Disabled { timezone: _ } => {
703 std::sync::Arc::new(crate::timezone::FixedClock::new(
708 chrono::DateTime::UNIX_EPOCH,
709 crate::timezone::TimeZoneSpec::Utc,
710 ))
711 }
712 DeterministicMode::Enabled {
713 timestamp_utc,
714 timezone,
715 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
716 *timestamp_utc,
717 timezone.clone(),
718 )),
719 })
720 }
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
725pub enum FormulaParsePolicy {
726 Strict,
728 CoerceToError,
730 KeepCachedValue,
732 AsText,
734}
735
736#[derive(Debug, Clone, PartialEq, Eq)]
738pub struct FormulaParseDiagnostic {
739 pub sheet: String,
740 pub row: u32,
741 pub col: u32,
742 pub formula: String,
743 pub message: String,
744 pub policy: FormulaParsePolicy,
745}
746
747#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
748pub enum FormulaPlaneMode {
749 #[default]
751 Off,
752 Shadow,
754 AuthoritativeExperimental,
756}
757
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub enum FormulaSpoolDiskPolicy {
761 NativeSpill,
763 MemoryOnly,
765}
766
767#[derive(Debug, Clone, PartialEq, Eq)]
769pub struct WorkbookLoadLimits {
770 pub max_sheet_rows: u32,
772 pub max_sheet_cols: u32,
774 pub max_sheet_logical_cells: u64,
776 pub max_formula_plane_fallback_cells: u64,
779 pub sparse_sheet_cell_threshold: u64,
781 pub max_sparse_cell_ratio: u64,
783 pub max_formula_spool_bytes_per_sheet: u64,
785 pub max_formula_spool_bytes_per_workbook: u64,
787 pub max_formula_spool_files_per_workbook: u32,
789 pub formula_spool_memory_prefix_bytes: u64,
791 pub max_formula_spool_memory_bytes: u64,
793 pub formula_spool_disk_policy: FormulaSpoolDiskPolicy,
795}
796
797impl Default for WorkbookLoadLimits {
798 fn default() -> Self {
799 Self {
800 max_sheet_rows: 1_048_576,
801 max_sheet_cols: 16_384,
802 max_sheet_logical_cells: 128_000_000,
803 max_formula_plane_fallback_cells: 2_000_000,
804 sparse_sheet_cell_threshold: 250_000,
805 max_sparse_cell_ratio: 1_024,
806 max_formula_spool_bytes_per_sheet: 256 * 1024 * 1024,
807 max_formula_spool_bytes_per_workbook: 1024 * 1024 * 1024,
808 max_formula_spool_files_per_workbook: 1_024,
809 formula_spool_memory_prefix_bytes: 1024 * 1024,
810 max_formula_spool_memory_bytes: 16 * 1024 * 1024,
811 formula_spool_disk_policy: if cfg!(target_arch = "wasm32") {
812 FormulaSpoolDiskPolicy::MemoryOnly
813 } else {
814 FormulaSpoolDiskPolicy::NativeSpill
815 },
816 }
817 }
818}
819
820#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
822pub enum TemporalEgress {
823 #[default]
824 Native,
825 Serial,
826}
827
828#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
831pub enum PreparationPolicy {
832 Strict,
835 #[default]
839 BestEffort,
840}
841
842#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
855#[non_exhaustive]
856pub enum NumericProfile {
857 #[default]
861 ExcelCompatible,
862}
863
864#[derive(Debug, Clone)]
866pub struct EvalConfig {
867 pub enable_parallel: bool,
868 pub max_threads: Option<usize>,
869 pub max_vertices: Option<usize>,
872 pub max_eval_time: Option<std::time::Duration>,
875 pub max_memory_mb: Option<usize>,
879 pub evaluation_budgets: EvaluationBudgets,
883
884 pub default_sheet_name: String,
886
887 pub case_sensitive_names: bool,
891
892 pub case_sensitive_tables: bool,
896
897 pub workbook_seed: u64,
899
900 pub volatile_level: VolatileLevel,
902
903 pub deterministic_mode: DeterministicMode,
905
906 pub range_expansion_limit: usize,
909
910 pub max_open_ended_rows: u32,
914
915 pub max_open_ended_cols: u32,
919
920 pub stripe_height: u32,
922 pub stripe_width: u32,
924 pub enable_block_stripes: bool,
928
929 pub spill: SpillConfig,
931
932 pub cycle: CycleConfig,
936
937 pub use_dynamic_topo: bool,
942 pub pk_visit_budget: usize,
944 pub pk_compaction_interval_ops: u64,
946 pub max_layer_width: Option<usize>,
948 pub pk_reject_cycle_edges: bool,
951 pub sheet_index_mode: SheetIndexMode,
953
954 pub warmup: tuning::WarmupConfig,
956
957 pub arrow_storage_enabled: bool,
959 pub delta_overlay_enabled: bool,
961
962 pub write_formula_overlay_enabled: bool,
965
966 pub max_overlay_memory_bytes: Option<usize>,
971
972 pub date_system: DateSystem,
974
975 pub temporal_egress: TemporalEgress,
977
978 pub formula_parse_policy: FormulaParsePolicy,
980
981 pub defer_graph_building: bool,
984
985 pub preparation_policy: PreparationPolicy,
988
989 pub enable_virtual_dep_telemetry: bool,
993
994 pub formula_plane_mode: FormulaPlaneMode,
998 pub max_formula_plane_cache_candidates: usize,
1000 pub max_formula_plane_cache_edges: usize,
1002 pub max_formula_plane_cache_bytes: usize,
1004
1005 pub lookup_index_cache_max_bytes: usize,
1007
1008 pub family_execution: bool,
1013
1014 pub family_kernels: bool,
1018
1019 pub family_lift: bool,
1024
1025 pub formula_compression: bool,
1031
1032 pub numeric_profile: NumericProfile,
1034}
1035
1036impl Default for EvalConfig {
1037 fn default() -> Self {
1038 Self {
1039 enable_parallel: true,
1040 max_threads: None,
1041 max_vertices: None,
1042 max_eval_time: None,
1043 max_memory_mb: None,
1044 evaluation_budgets: EvaluationBudgets::default(),
1045
1046 default_sheet_name: format!("Sheet{}", 1),
1047
1048 case_sensitive_names: false,
1050 case_sensitive_tables: false,
1051
1052 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
1054
1055 volatile_level: VolatileLevel::Always,
1057
1058 deterministic_mode: DeterministicMode::default(),
1059
1060 range_expansion_limit: 64,
1062 max_open_ended_rows: 1_048_576,
1065 max_open_ended_cols: 16_384,
1066 stripe_height: 256,
1067 stripe_width: 256,
1068 enable_block_stripes: false,
1069 spill: SpillConfig::default(),
1070 cycle: CycleConfig::default(),
1071
1072 use_dynamic_topo: false, pk_visit_budget: 50_000,
1075 pk_compaction_interval_ops: 100_000,
1076 max_layer_width: None,
1077 pk_reject_cycle_edges: false,
1078 sheet_index_mode: SheetIndexMode::Eager,
1079 warmup: tuning::WarmupConfig::default(),
1080 arrow_storage_enabled: true,
1081 delta_overlay_enabled: true,
1082 write_formula_overlay_enabled: true,
1083 max_overlay_memory_bytes: None,
1084 date_system: DateSystem::Excel1900,
1085 temporal_egress: TemporalEgress::default(),
1086 formula_parse_policy: FormulaParsePolicy::Strict,
1087 defer_graph_building: false,
1088 preparation_policy: PreparationPolicy::BestEffort,
1089 enable_virtual_dep_telemetry: false,
1090 formula_plane_mode: FormulaPlaneMode::Off,
1091 max_formula_plane_cache_candidates: 100_000,
1092 max_formula_plane_cache_edges: 100_000,
1093 max_formula_plane_cache_bytes: 64 * 1024 * 1024,
1094 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
1095 family_execution: true,
1096 family_kernels: true,
1097 family_lift: true,
1098 formula_compression: true,
1099 numeric_profile: NumericProfile::ExcelCompatible,
1100 }
1101 }
1102}
1103
1104impl EvalConfig {
1105 #[inline]
1106 pub fn with_preparation_policy(mut self, policy: PreparationPolicy) -> Self {
1107 self.preparation_policy = policy;
1108 self
1109 }
1110
1111 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
1112 self.range_expansion_limit = limit;
1113 self
1114 }
1115
1116 #[inline]
1117 pub fn with_parallel(mut self, enable: bool) -> Self {
1118 self.enable_parallel = enable;
1119 self
1120 }
1121
1122 #[inline]
1123 pub fn with_block_stripes(mut self, enable: bool) -> Self {
1124 self.enable_block_stripes = enable;
1125 self
1126 }
1127
1128 #[inline]
1129 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
1130 self.case_sensitive_names = enable;
1131 self
1132 }
1133
1134 #[inline]
1135 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
1136 self.case_sensitive_tables = enable;
1137 self
1138 }
1139
1140 #[inline]
1141 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
1142 self.arrow_storage_enabled = enable;
1143 self
1144 }
1145
1146 #[inline]
1147 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
1148 self.delta_overlay_enabled = enable;
1149 self
1150 }
1151
1152 #[inline]
1153 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
1154 self.write_formula_overlay_enabled = enable;
1155 self
1156 }
1157
1158 #[inline]
1159 pub fn with_date_system(mut self, system: DateSystem) -> Self {
1160 self.date_system = system;
1161 self
1162 }
1163
1164 #[inline]
1165 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
1166 self.formula_parse_policy = policy;
1167 self
1168 }
1169
1170 #[inline]
1171 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
1172 self.enable_virtual_dep_telemetry = enable;
1173 self
1174 }
1175
1176 #[inline]
1177 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
1178 self.formula_plane_mode = mode;
1179 self
1180 }
1181
1182 #[inline]
1183 pub fn with_evaluation_budgets(mut self, budgets: EvaluationBudgets) -> Self {
1184 self.evaluation_budgets = budgets;
1185 self
1186 }
1187
1188 pub fn resolved_evaluation_budgets(&self) -> EvaluationBudgets {
1190 resource_ledger::resolve_evaluation_budgets(
1191 &self.evaluation_budgets,
1192 self.max_vertices,
1193 self.max_memory_mb,
1194 self.max_eval_time,
1195 )
1196 .budgets
1197 }
1198
1199 #[inline]
1208 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
1209 if let Err(msg) = cycle.validate() {
1210 panic!("invalid CycleConfig: {msg}");
1211 }
1212 self.cycle = cycle;
1213 self
1214 }
1215}
1216
1217#[derive(Debug, Clone, Copy, PartialEq, Default)]
1222pub struct CycleConfig {
1223 pub detection: CycleDetection,
1224 pub policy: CyclePolicy,
1225}
1226
1227impl CycleConfig {
1228 pub fn iterate_excel_defaults() -> Self {
1231 Self {
1232 detection: CycleDetection::Runtime,
1233 policy: CyclePolicy::iterate_excel_defaults(),
1234 }
1235 }
1236
1237 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
1239 Self {
1240 detection: CycleDetection::Runtime,
1241 policy: CyclePolicy::Iterate {
1242 max_iterations,
1243 max_change,
1244 },
1245 }
1246 }
1247
1248 pub fn validate(&self) -> Result<(), String> {
1252 if let CyclePolicy::Iterate {
1253 max_iterations,
1254 max_change,
1255 } = self.policy
1256 {
1257 if self.detection == CycleDetection::Static {
1258 return Err(
1259 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
1260 );
1261 }
1262 if max_iterations == 0 {
1263 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
1264 }
1265 if !max_change.is_finite() || max_change < 0.0 {
1266 return Err(format!(
1267 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
1268 ));
1269 }
1270 }
1271 Ok(())
1272 }
1273
1274 #[inline]
1279 pub(crate) fn allows_self_dependency(&self) -> bool {
1280 self.detection == CycleDetection::Runtime
1281 && matches!(self.policy, CyclePolicy::Iterate { .. })
1282 }
1283}
1284
1285#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1287pub enum CycleDetection {
1288 #[default]
1291 Static,
1292 Runtime,
1296}
1297
1298#[derive(Debug, Clone, Copy, PartialEq, Default)]
1300pub enum CyclePolicy {
1301 #[default]
1303 Error,
1304 Iterate {
1312 max_iterations: u32,
1316 max_change: f64,
1320 },
1321}
1322
1323impl CyclePolicy {
1324 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1326 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1328
1329 pub fn iterate_excel_defaults() -> Self {
1331 CyclePolicy::Iterate {
1332 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
1333 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
1334 }
1335 }
1336}
1337
1338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1339pub enum SheetIndexMode {
1340 Eager,
1342 Lazy,
1344 FastBatch,
1346}
1347
1348pub use formualizer_common::DateSystem;
1349
1350pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1352where
1353 R: EvaluationContext + 'static,
1354{
1355 Engine::new(resolver, config)
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1360pub struct SpillConfig {
1361 pub conflict_policy: SpillConflictPolicy,
1363 pub tiebreaker: SpillTiebreaker,
1365 pub bounds_policy: SpillBoundsPolicy,
1367 pub buffer_mode: SpillBufferMode,
1369 pub memory_budget_bytes: Option<u64>,
1371 pub cancellation: SpillCancellationPolicy,
1373 pub visibility: SpillVisibility,
1375
1376 pub max_spill_cells: u32,
1380}
1381
1382impl Default for SpillConfig {
1383 fn default() -> Self {
1384 Self {
1385 conflict_policy: SpillConflictPolicy::Error,
1386 tiebreaker: SpillTiebreaker::FirstWins,
1387 bounds_policy: SpillBoundsPolicy::Strict,
1388 buffer_mode: SpillBufferMode::ShadowBuffer,
1389 memory_budget_bytes: None,
1390 cancellation: SpillCancellationPolicy::Cooperative,
1391 visibility: SpillVisibility::OnCommit,
1392 max_spill_cells: 10_000,
1394 }
1395 }
1396}
1397
1398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1399pub enum SpillConflictPolicy {
1400 Error,
1401 Preempt,
1402}
1403
1404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1405pub enum SpillTiebreaker {
1406 FirstWins,
1407 EvaluationEpochAsc,
1408 AnchorAddressAsc,
1409 FunctionPriorityThenAddress,
1410}
1411
1412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1413pub enum SpillBoundsPolicy {
1414 Strict,
1415 Truncate,
1416}
1417
1418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1419pub enum SpillBufferMode {
1420 ShadowBuffer,
1421 PersistenceJournal,
1422}
1423
1424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1425pub enum SpillCancellationPolicy {
1426 Cooperative,
1427 Strict,
1428}
1429
1430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1431pub enum SpillVisibility {
1432 OnCommit,
1433 StagedLayer,
1434}
1435
1436#[derive(Debug, Default)]
1447pub struct TombstoneRegistry {
1448 pub pending_references: HashMap<String, Vec<VertexId>>,
1450}
1451
1452impl TombstoneRegistry {
1453 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1455 self.pending_references
1456 .entry(sheet_name)
1457 .or_default()
1458 .push(vertex_id);
1459 }
1460
1461 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1463 self.pending_references
1464 .remove(sheet_name)
1465 .unwrap_or_default()
1466 }
1467}