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;
40mod source_family;
41pub mod spill;
42mod target_preparation;
43#[doc(hidden)]
44pub mod template;
45pub(crate) mod used_extent;
46pub mod vertex;
47pub mod virtual_deps;
48
49#[cfg(any(test, feature = "legacy_oracle"))]
53pub mod csr_edges;
54pub mod debug_views;
55#[cfg(any(test, feature = "legacy_oracle"))]
58pub mod delta_edges;
59pub mod interval_tree;
60pub mod named_range;
61pub mod sheet_index;
62pub mod sheet_registry;
63#[cfg(any(test, feature = "legacy_oracle"))]
66pub mod topo;
67pub(crate) mod trace;
68pub mod vertex_store;
69
70pub mod arena;
72
73pub mod tuning;
75
76#[cfg(test)]
77mod tests;
78
79pub use arena::AstNodeId;
80pub use cancel::CancelToken;
81pub use eval::{
82 CycleTelemetry, Engine, EngineAction, EngineBaselineStats, EvalResult, RecalcPlan,
83 SourceFormulaIngress, TableMetadata, VirtualDepTelemetry,
84};
85pub use eval_delta::{
86 DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy, EvalDeltaRecord, TARGET_EVAL_DELTA_VERSION,
87 TargetEvalDelta,
88};
89pub use formula_ingest::{
90 FormulaFamilyGrouper, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
91};
92#[doc(hidden)]
93pub use formula_source::{
94 DeferredFormulaPackage, DeferredFormulaReplay, DeferredReplayFormula,
95 ExplicitPartitionLegacyMembers, ExplicitSourceFamilyMembers, FormulaCompressedPreparation,
96 FormulaCompressedSourceBatch, FormulaCompressedSourceReport,
97 FormulaReplayCoordinateDisposition, FormulaReplayDisposition, FormulaReplayPartitionRouter,
98 MAX_EXPLICIT_SOURCE_FAMILY_MEMBERS, MAX_PARTITIONED_SOURCE_FAMILY_FRAGMENTS,
99 PartitionLegacyMember, PartitionLegacyMemberKind, PartitionReconciliation,
100 PartitionedSourceFormulaFamily, PlacementDomainTransport, SourceCoord, SourceFamilyId,
101 SourceFamilyMembers, SourceFormulaFamily, SourceFormulaOrder, SourceRect,
102};
103pub use journal::{ActionJournal, ArrowOp, ArrowUndoBatch, GraphUndoBatch};
104#[doc(hidden)]
105pub use source_family::{SourceFamilyCounters, SourceFamilyMode, source_family_process_totals};
106#[allow(deprecated)]
107pub use target_preparation::PrepareTargetsOptions;
108pub use formualizer_common::{ResourceExhaustionDetail, ResourceExhaustionReason};
110pub use graph::FormulaView;
111pub use graph::snapshot::VertexSnapshot;
112pub use graph::{
113 ChangeEvent, DependencyGraph, DependencyRef, GraphBaselineStats, OperationSummary, StripeKey,
114 StripeType, block_index,
115};
116pub use resource_ledger::{
117 AdmissionResourceBudget, DeadlineResourceBudget, DiskScratchPolicy, EvaluationBudgets,
118 EvaluationIncompleteReason, EvaluationResourceConfigDiagnostic,
119 LegacyResourceConfigDisposition, OptimizationResourceBudget, ResourceEnvelope,
120 ResourceLedgerError, ResourceLedgerSnapshot, RetainedResourceBudget, ScratchResourceBudget,
121 SemanticResourceBudget, WorkResourceBudget,
122};
123pub(crate) use resource_ledger::ResourceLedger;
126pub use resource_observability::{
127 EvaluationRequestKind, EvaluationRequestOutcome, EvaluationRequestPhaseTimings,
128 EvaluationResourceBaselineStats, EvaluationResourceClass, EvaluationResourceLedgerRequestStats,
129 EvaluationResourceReason, EvaluationResourceRequestStats, FormulaDirtyLeaseOutcome,
130 FormulaPlaneRoute, FormulaPlaneRouteEvent, FormulaPlaneRoutePhase,
131 FormulaPlaneRouteTransitionReason, FormulaPlaneTopologyCacheOutcome,
132 FormulaPlaneTopologyRequestStats, FormulaPlaneTopologyStrategy,
133};
134pub use row_visibility::{RowVisibilitySource, VisibilityMaskMode};
135#[cfg(any(test, feature = "legacy_oracle"))]
138pub use scheduler::Scheduler;
139pub use scheduler::{Layer, Schedule, ScheduleUnit};
140pub use target_preparation::{
141 EvaluationTarget, OpaquePreparePolicy, OpaqueReason, PreparationOutcome, PreparationRevision,
142 PrepareScope, PreparedTargetGraphReport, RequestId, TableSelection, TargetEvalOptions,
143};
144pub use vertex::{VertexId, VertexKind};
145
146pub use graph::editor::{
147 DataUpdateSummary, EditorError, MetaUpdateSummary, RangeSummary, ShiftSummary, TransactionId,
148 VertexDataPatch, VertexEditor, VertexMeta, VertexMetaPatch,
149};
150
151pub use graph::editor::change_log::{ChangeLog, ChangeLogger, NullChangeLogger};
152
153#[doc(hidden)]
154pub mod fp8_parity_test_support {
155 use super::{Engine, EvalConfig};
156 use crate::engine::arena::CanonicalLabels;
157 use crate::engine::template::canonical::{
158 CanonicalRejectReason, CanonicalTemplateFlag, canonicalize_template,
159 };
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 Fp8ParityObservation {
297 formula: formula.to_string(),
298 placement,
299 old_payload: old.payload,
300 new_hash: old.canonical_hash,
301 }
302 }
303 (Err(old), Err(new)) => {
304 assert_eq!(
305 old.kind.to_string(),
306 new.kind.to_string(),
307 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
308 );
309 Fp8ParityObservation {
310 formula: formula.to_string(),
311 placement,
312 old_payload: format!("ERR:{:?}", old.kind),
313 new_hash: 0,
314 }
315 }
316 (Ok(_), Err(new)) => panic!(
317 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
318 ),
319 (Err(old), Ok(_)) => panic!(
320 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
321 ),
322 }
323 }
324
325 #[derive(Debug)]
326 struct OldOutput {
327 payload: String,
328 direct_cells: Vec<CellRef>,
329 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
330 unresolved_names: Vec<String>,
331 volatile: bool,
332 dynamic: bool,
333 canonical_hash: u64,
334 }
335
336 fn old_path<R: EvaluationContext>(
337 engine: &mut Engine<R>,
338 ast: &ASTNode,
339 placement: CellRef,
340 ) -> Result<OldOutput, ExcelError> {
341 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
342 .graph
343 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
344 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
345 let dynamic = engine.graph.is_ast_dynamic(ast);
346 let template =
347 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
348 engine.intern_formula_ast(ast);
351 let metadata = engine.ingest_pipeline().parity_metadata(ast, placement);
352 let mut expected = canonical_labels_from_old(&template.labels);
353 if dynamic {
354 expected.flags |= CanonicalLabels::FLAG_DYNAMIC;
355 }
356 assert_eq!(
357 expected.flags, metadata.labels.flags,
358 "canonical flags for {ast:?}"
359 );
360 assert_eq!(
361 expected.rejects, metadata.labels.rejects,
362 "canonical rejects for {ast:?}"
363 );
364 Ok(OldOutput {
365 payload: template.key.payload().to_string(),
366 direct_cells: sorted_cells(placeholders),
367 range_deps: ranges,
368 unresolved_names,
369 volatile,
370 dynamic,
371 canonical_hash: metadata.canonical_hash,
372 })
373 }
374
375 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
376 cells.sort();
377 cells.dedup();
378 cells
379 }
380
381 fn canonical_labels_from_old(
382 old: &crate::engine::template::canonical::CanonicalTemplateLabels,
383 ) -> CanonicalLabels {
384 let mut labels = CanonicalLabels::default();
385 for flag in &old.flags {
386 labels.flags |= match flag {
387 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
388 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
389 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
390 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
391 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
392 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
393 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
394 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
395 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
396 };
397 }
398 for reason in &old.reject_reasons {
399 labels.flags |= match reason {
400 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
401 CanonicalLabels::FLAG_DYNAMIC
402 }
403 CanonicalRejectReason::ParserVolatileFlag
404 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
405 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
406 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
407 }
408 CanonicalRejectReason::ArrayOrSpillFunction { .. }
409 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
410 CanonicalRejectReason::StructuredReference { .. }
411 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
412 CanonicalLabels::FLAG_CONTAINS_TABLE
413 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
414 }
415 CanonicalRejectReason::OpenRangeReference { .. }
416 | CanonicalRejectReason::WholeAxisReference { .. } => {
417 CanonicalLabels::FLAG_CONTAINS_RANGE
418 }
419 _ => 0,
420 };
421 labels.rejects |= match reason {
422 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
423 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
424 }
425 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
426 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
427 }
428 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
429 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
430 }
431 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
432 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
433 }
434 CanonicalRejectReason::ParserVolatileFlag => {
435 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
436 }
437 CanonicalRejectReason::VolatileFunction { .. } => {
438 CanonicalLabels::REJECT_VOLATILE_FUNCTION
439 }
440 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
441 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
442 }
443 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
444 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
445 }
446 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
447 CanonicalRejectReason::SpillReference { .. } => {
448 CanonicalLabels::REJECT_SPILL_REFERENCE
449 }
450 CanonicalRejectReason::SpillResultRegionOperator => {
451 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
452 }
453 CanonicalRejectReason::ImplicitIntersectionOperator => {
454 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
455 }
456 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
457 CanonicalRejectReason::StructuredReference { .. } => {
458 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
459 }
460 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
461 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
462 }
463 CanonicalRejectReason::ThreeDReference { .. } => {
464 CanonicalLabels::REJECT_THREE_D_REFERENCE
465 }
466 CanonicalRejectReason::ExternalReference { .. } => {
467 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
468 }
469 CanonicalRejectReason::OpenRangeReference { .. } => {
470 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
471 }
472 CanonicalRejectReason::WholeAxisReference { .. } => {
473 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
474 }
475 CanonicalRejectReason::UnsupportedReference { .. } => {
476 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
477 }
478 CanonicalRejectReason::FunctionContractUnsupported { .. }
479 | CanonicalRejectReason::ContextDependentFunction { .. } => {
480 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
481 }
482 };
483 }
484 labels
485 }
486
487 pub fn literal_number(value: f64) -> LiteralValue {
488 LiteralValue::Number(value)
489 }
490}
491
492use crate::timezone::TimeZoneSpec;
495use crate::traits::EvaluationContext;
496use crate::traits::VolatileLevel;
497use chrono::{DateTime, Utc};
498use formualizer_common::error::{ExcelError, ExcelErrorKind};
499use std::collections::HashMap;
500
501impl<R: EvaluationContext> Engine<R> {
502 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
503 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
504 }
505
506 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
507 self.graph.store_ast(ast)
508 }
509
510 pub fn stage_formula_ast(
519 &mut self,
520 grouper: &mut FormulaFamilyGrouper,
521 row: u32,
522 col: u32,
523 ast: &formualizer_parse::parser::ASTNode,
524 formula_text: Option<std::sync::Arc<str>>,
525 ) -> FormulaIngestRecord {
526 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
527 if self.config.formula_compression
528 && let Some(family) = self.graph.group_formula_member(grouper, row0, col0, ast)
529 {
530 let record = FormulaIngestRecord::member(row, col, family.template, family.anchor);
531 grouper.members += 1;
532 grouper.counters.parsed_members += 1;
533 grouper.note(row0, col0, family);
534 return record;
535 }
536 let ast_id = self.intern_formula_ast(ast);
537 self.note_staged_formula(grouper, row, col, ast_id);
538 FormulaIngestRecord::new(row, col, ast_id, formula_text)
539 }
540
541 #[doc(hidden)]
544 pub fn source_family_mode(&self) -> SourceFamilyMode {
545 self.source_family_mode
546 }
547
548 #[doc(hidden)]
549 pub fn set_source_family_mode(&mut self, mode: SourceFamilyMode) {
550 self.source_family_mode = mode;
551 }
552
553 #[doc(hidden)]
555 pub fn source_family_counters(&self) -> SourceFamilyCounters {
556 self.source_family_counters
557 }
558
559 #[doc(hidden)]
562 pub fn finish_family_grouper(&mut self, grouper: &mut FormulaFamilyGrouper) {
563 let counters = std::mem::take(&mut grouper.counters);
564 counters.publish();
565 self.source_family_counters.accumulate(&counters);
566 }
567
568 pub fn note_staged_formula(
571 &mut self,
572 grouper: &mut FormulaFamilyGrouper,
573 row: u32,
574 col: u32,
575 ast_id: AstNodeId,
576 ) {
577 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
578 grouper.note(
579 row0,
580 col0,
581 formula_ingest::GroupedFamily {
582 template: ast_id,
583 anchor: (row0, col0),
584 rendered: None,
585 lexical: None,
586 },
587 );
588 }
589}
590
591pub trait CalcObserver: Send + Sync {
593 fn on_eval_start(&self, vertex_id: VertexId);
594 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
595 fn on_cycle_detected(&self, cycle: &[VertexId]);
596 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
597}
598
599impl CalcObserver for () {
601 fn on_eval_start(&self, _vertex_id: VertexId) {}
602 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
603 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
604 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
605}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
611pub enum DeterministicMode {
612 Disabled {
614 timezone: TimeZoneSpec,
616 },
617 Enabled {
619 timestamp_utc: DateTime<Utc>,
621 timezone: TimeZoneSpec,
623 },
624}
625
626impl Default for DeterministicMode {
627 fn default() -> Self {
628 Self::Disabled {
629 timezone: TimeZoneSpec::default(),
630 }
631 }
632}
633
634impl DeterministicMode {
635 pub fn is_enabled(&self) -> bool {
636 matches!(self, DeterministicMode::Enabled { .. })
637 }
638
639 pub fn timezone(&self) -> &TimeZoneSpec {
640 match self {
641 DeterministicMode::Disabled { timezone } => timezone,
642 DeterministicMode::Enabled { timezone, .. } => timezone,
643 }
644 }
645
646 pub fn validate(&self) -> Result<(), ExcelError> {
647 if let DeterministicMode::Enabled { timezone, .. } = self {
648 timezone
649 .validate_for_determinism()
650 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
651 }
652 Ok(())
653 }
654
655 pub fn build_clock(
656 &self,
657 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
658 self.validate()?;
659 Ok(match self {
660 #[cfg(feature = "system-clock")]
661 DeterministicMode::Disabled { timezone } => {
662 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
663 }
664 #[cfg(not(feature = "system-clock"))]
665 DeterministicMode::Disabled { timezone: _ } => {
666 std::sync::Arc::new(crate::timezone::FixedClock::new(
671 chrono::DateTime::UNIX_EPOCH,
672 crate::timezone::TimeZoneSpec::Utc,
673 ))
674 }
675 DeterministicMode::Enabled {
676 timestamp_utc,
677 timezone,
678 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
679 *timestamp_utc,
680 timezone.clone(),
681 )),
682 })
683 }
684}
685
686#[derive(Debug, Clone, Copy, PartialEq, Eq)]
688pub enum FormulaParsePolicy {
689 Strict,
691 CoerceToError,
693 KeepCachedValue,
695 AsText,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq)]
701pub struct FormulaParseDiagnostic {
702 pub sheet: String,
703 pub row: u32,
704 pub col: u32,
705 pub formula: String,
706 pub message: String,
707 pub policy: FormulaParsePolicy,
708}
709
710#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
711pub enum FormulaPlaneMode {
712 #[default]
714 Off,
715 Shadow,
717 AuthoritativeExperimental,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq)]
723pub enum FormulaSpoolDiskPolicy {
724 NativeSpill,
726 MemoryOnly,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq)]
732pub struct WorkbookLoadLimits {
733 pub max_sheet_rows: u32,
735 pub max_sheet_cols: u32,
737 pub max_sheet_logical_cells: u64,
739 pub max_formula_plane_fallback_cells: u64,
742 pub sparse_sheet_cell_threshold: u64,
744 pub max_sparse_cell_ratio: u64,
746 pub max_formula_spool_bytes_per_sheet: u64,
748 pub max_formula_spool_bytes_per_workbook: u64,
750 pub max_formula_spool_files_per_workbook: u32,
752 pub formula_spool_memory_prefix_bytes: u64,
754 pub max_formula_spool_memory_bytes: u64,
756 pub formula_spool_disk_policy: FormulaSpoolDiskPolicy,
758}
759
760impl Default for WorkbookLoadLimits {
761 fn default() -> Self {
762 Self {
763 max_sheet_rows: 1_048_576,
764 max_sheet_cols: 16_384,
765 max_sheet_logical_cells: 128_000_000,
766 max_formula_plane_fallback_cells: 2_000_000,
767 sparse_sheet_cell_threshold: 250_000,
768 max_sparse_cell_ratio: 1_024,
769 max_formula_spool_bytes_per_sheet: 256 * 1024 * 1024,
770 max_formula_spool_bytes_per_workbook: 1024 * 1024 * 1024,
771 max_formula_spool_files_per_workbook: 1_024,
772 formula_spool_memory_prefix_bytes: 1024 * 1024,
773 max_formula_spool_memory_bytes: 16 * 1024 * 1024,
774 formula_spool_disk_policy: if cfg!(target_arch = "wasm32") {
775 FormulaSpoolDiskPolicy::MemoryOnly
776 } else {
777 FormulaSpoolDiskPolicy::NativeSpill
778 },
779 }
780 }
781}
782
783#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
785pub enum TemporalEgress {
786 #[default]
787 Native,
788 Serial,
789}
790
791#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
794pub enum PreparationPolicy {
795 Strict,
798 #[default]
802 BestEffort,
803}
804
805#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
818#[non_exhaustive]
819pub enum NumericProfile {
820 #[default]
824 ExcelCompatible,
825}
826
827#[derive(Debug, Clone)]
829pub struct EvalConfig {
830 pub enable_parallel: bool,
831 pub max_threads: Option<usize>,
832 pub max_vertices: Option<usize>,
835 pub max_eval_time: Option<std::time::Duration>,
838 pub max_memory_mb: Option<usize>,
842 pub evaluation_budgets: EvaluationBudgets,
846
847 pub default_sheet_name: String,
849
850 pub case_sensitive_names: bool,
854
855 pub case_sensitive_tables: bool,
859
860 pub workbook_seed: u64,
862
863 pub volatile_level: VolatileLevel,
865
866 pub deterministic_mode: DeterministicMode,
868
869 pub range_expansion_limit: usize,
872
873 pub max_open_ended_rows: u32,
877
878 pub max_open_ended_cols: u32,
882
883 pub stripe_height: u32,
885 pub stripe_width: u32,
887 pub enable_block_stripes: bool,
891
892 pub spill: SpillConfig,
894
895 pub cycle: CycleConfig,
899
900 pub use_dynamic_topo: bool,
905 pub pk_visit_budget: usize,
907 pub pk_compaction_interval_ops: u64,
909 pub max_layer_width: Option<usize>,
911 pub pk_reject_cycle_edges: bool,
914 pub sheet_index_mode: SheetIndexMode,
916
917 pub warmup: tuning::WarmupConfig,
919
920 pub arrow_storage_enabled: bool,
922 pub delta_overlay_enabled: bool,
924
925 pub write_formula_overlay_enabled: bool,
928
929 pub max_overlay_memory_bytes: Option<usize>,
934
935 pub date_system: DateSystem,
937
938 pub temporal_egress: TemporalEgress,
940
941 pub formula_parse_policy: FormulaParsePolicy,
943
944 pub defer_graph_building: bool,
947
948 pub preparation_policy: PreparationPolicy,
951
952 pub enable_virtual_dep_telemetry: bool,
956
957 pub formula_plane_mode: FormulaPlaneMode,
961 pub max_formula_plane_cache_candidates: usize,
963 pub max_formula_plane_cache_edges: usize,
965 pub max_formula_plane_cache_bytes: usize,
967
968 pub lookup_index_cache_max_bytes: usize,
970
971 pub family_execution: bool,
976
977 pub family_kernels: bool,
981
982 pub family_lift: bool,
987
988 pub formula_compression: bool,
994
995 pub numeric_profile: NumericProfile,
997}
998
999impl Default for EvalConfig {
1000 fn default() -> Self {
1001 Self {
1002 enable_parallel: true,
1003 max_threads: None,
1004 max_vertices: None,
1005 max_eval_time: None,
1006 max_memory_mb: None,
1007 evaluation_budgets: EvaluationBudgets::default(),
1008
1009 default_sheet_name: format!("Sheet{}", 1),
1010
1011 case_sensitive_names: false,
1013 case_sensitive_tables: false,
1014
1015 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
1017
1018 volatile_level: VolatileLevel::Always,
1020
1021 deterministic_mode: DeterministicMode::default(),
1022
1023 range_expansion_limit: 64,
1025 max_open_ended_rows: 1_048_576,
1028 max_open_ended_cols: 16_384,
1029 stripe_height: 256,
1030 stripe_width: 256,
1031 enable_block_stripes: false,
1032 spill: SpillConfig::default(),
1033 cycle: CycleConfig::default(),
1034
1035 use_dynamic_topo: false, pk_visit_budget: 50_000,
1038 pk_compaction_interval_ops: 100_000,
1039 max_layer_width: None,
1040 pk_reject_cycle_edges: false,
1041 sheet_index_mode: SheetIndexMode::Eager,
1042 warmup: tuning::WarmupConfig::default(),
1043 arrow_storage_enabled: true,
1044 delta_overlay_enabled: true,
1045 write_formula_overlay_enabled: true,
1046 max_overlay_memory_bytes: None,
1047 date_system: DateSystem::Excel1900,
1048 temporal_egress: TemporalEgress::default(),
1049 formula_parse_policy: FormulaParsePolicy::Strict,
1050 defer_graph_building: false,
1051 preparation_policy: PreparationPolicy::BestEffort,
1052 enable_virtual_dep_telemetry: false,
1053 formula_plane_mode: FormulaPlaneMode::Off,
1054 max_formula_plane_cache_candidates: 100_000,
1055 max_formula_plane_cache_edges: 100_000,
1056 max_formula_plane_cache_bytes: 64 * 1024 * 1024,
1057 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
1058 family_execution: true,
1059 family_kernels: true,
1060 family_lift: true,
1061 formula_compression: true,
1062 numeric_profile: NumericProfile::ExcelCompatible,
1063 }
1064 }
1065}
1066
1067impl EvalConfig {
1068 #[inline]
1069 pub fn with_preparation_policy(mut self, policy: PreparationPolicy) -> Self {
1070 self.preparation_policy = policy;
1071 self
1072 }
1073
1074 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
1075 self.range_expansion_limit = limit;
1076 self
1077 }
1078
1079 #[inline]
1080 pub fn with_parallel(mut self, enable: bool) -> Self {
1081 self.enable_parallel = enable;
1082 self
1083 }
1084
1085 #[inline]
1086 pub fn with_block_stripes(mut self, enable: bool) -> Self {
1087 self.enable_block_stripes = enable;
1088 self
1089 }
1090
1091 #[inline]
1092 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
1093 self.case_sensitive_names = enable;
1094 self
1095 }
1096
1097 #[inline]
1098 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
1099 self.case_sensitive_tables = enable;
1100 self
1101 }
1102
1103 #[inline]
1104 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
1105 self.arrow_storage_enabled = enable;
1106 self
1107 }
1108
1109 #[inline]
1110 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
1111 self.delta_overlay_enabled = enable;
1112 self
1113 }
1114
1115 #[inline]
1116 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
1117 self.write_formula_overlay_enabled = enable;
1118 self
1119 }
1120
1121 #[inline]
1122 pub fn with_date_system(mut self, system: DateSystem) -> Self {
1123 self.date_system = system;
1124 self
1125 }
1126
1127 #[inline]
1128 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
1129 self.formula_parse_policy = policy;
1130 self
1131 }
1132
1133 #[inline]
1134 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
1135 self.enable_virtual_dep_telemetry = enable;
1136 self
1137 }
1138
1139 #[inline]
1140 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
1141 self.formula_plane_mode = mode;
1142 self
1143 }
1144
1145 #[inline]
1146 pub fn with_evaluation_budgets(mut self, budgets: EvaluationBudgets) -> Self {
1147 self.evaluation_budgets = budgets;
1148 self
1149 }
1150
1151 pub fn resolved_evaluation_budgets(&self) -> EvaluationBudgets {
1153 resource_ledger::resolve_evaluation_budgets(
1154 &self.evaluation_budgets,
1155 self.max_vertices,
1156 self.max_memory_mb,
1157 self.max_eval_time,
1158 )
1159 .budgets
1160 }
1161
1162 #[inline]
1171 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
1172 if let Err(msg) = cycle.validate() {
1173 panic!("invalid CycleConfig: {msg}");
1174 }
1175 self.cycle = cycle;
1176 self
1177 }
1178}
1179
1180#[derive(Debug, Clone, Copy, PartialEq, Default)]
1185pub struct CycleConfig {
1186 pub detection: CycleDetection,
1187 pub policy: CyclePolicy,
1188}
1189
1190impl CycleConfig {
1191 pub fn iterate_excel_defaults() -> Self {
1194 Self {
1195 detection: CycleDetection::Runtime,
1196 policy: CyclePolicy::iterate_excel_defaults(),
1197 }
1198 }
1199
1200 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
1202 Self {
1203 detection: CycleDetection::Runtime,
1204 policy: CyclePolicy::Iterate {
1205 max_iterations,
1206 max_change,
1207 },
1208 }
1209 }
1210
1211 pub fn validate(&self) -> Result<(), String> {
1215 if let CyclePolicy::Iterate {
1216 max_iterations,
1217 max_change,
1218 } = self.policy
1219 {
1220 if self.detection == CycleDetection::Static {
1221 return Err(
1222 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
1223 );
1224 }
1225 if max_iterations == 0 {
1226 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
1227 }
1228 if !max_change.is_finite() || max_change < 0.0 {
1229 return Err(format!(
1230 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
1231 ));
1232 }
1233 }
1234 Ok(())
1235 }
1236
1237 #[inline]
1242 pub(crate) fn allows_self_dependency(&self) -> bool {
1243 self.detection == CycleDetection::Runtime
1244 && matches!(self.policy, CyclePolicy::Iterate { .. })
1245 }
1246}
1247
1248#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1250pub enum CycleDetection {
1251 #[default]
1254 Static,
1255 Runtime,
1259}
1260
1261#[derive(Debug, Clone, Copy, PartialEq, Default)]
1263pub enum CyclePolicy {
1264 #[default]
1266 Error,
1267 Iterate {
1275 max_iterations: u32,
1279 max_change: f64,
1283 },
1284}
1285
1286impl CyclePolicy {
1287 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1289 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1291
1292 pub fn iterate_excel_defaults() -> Self {
1294 CyclePolicy::Iterate {
1295 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
1296 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
1297 }
1298 }
1299}
1300
1301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1302pub enum SheetIndexMode {
1303 Eager,
1305 Lazy,
1307 FastBatch,
1309}
1310
1311pub use formualizer_common::DateSystem;
1312
1313pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1315where
1316 R: EvaluationContext + 'static,
1317{
1318 Engine::new(resolver, config)
1319}
1320
1321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1323pub struct SpillConfig {
1324 pub conflict_policy: SpillConflictPolicy,
1326 pub tiebreaker: SpillTiebreaker,
1328 pub bounds_policy: SpillBoundsPolicy,
1330 pub buffer_mode: SpillBufferMode,
1332 pub memory_budget_bytes: Option<u64>,
1334 pub cancellation: SpillCancellationPolicy,
1336 pub visibility: SpillVisibility,
1338
1339 pub max_spill_cells: u32,
1343}
1344
1345impl Default for SpillConfig {
1346 fn default() -> Self {
1347 Self {
1348 conflict_policy: SpillConflictPolicy::Error,
1349 tiebreaker: SpillTiebreaker::FirstWins,
1350 bounds_policy: SpillBoundsPolicy::Strict,
1351 buffer_mode: SpillBufferMode::ShadowBuffer,
1352 memory_budget_bytes: None,
1353 cancellation: SpillCancellationPolicy::Cooperative,
1354 visibility: SpillVisibility::OnCommit,
1355 max_spill_cells: 10_000,
1357 }
1358 }
1359}
1360
1361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1362pub enum SpillConflictPolicy {
1363 Error,
1364 Preempt,
1365}
1366
1367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1368pub enum SpillTiebreaker {
1369 FirstWins,
1370 EvaluationEpochAsc,
1371 AnchorAddressAsc,
1372 FunctionPriorityThenAddress,
1373}
1374
1375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1376pub enum SpillBoundsPolicy {
1377 Strict,
1378 Truncate,
1379}
1380
1381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1382pub enum SpillBufferMode {
1383 ShadowBuffer,
1384 PersistenceJournal,
1385}
1386
1387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1388pub enum SpillCancellationPolicy {
1389 Cooperative,
1390 Strict,
1391}
1392
1393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1394pub enum SpillVisibility {
1395 OnCommit,
1396 StagedLayer,
1397}
1398
1399#[derive(Debug, Default)]
1410pub struct TombstoneRegistry {
1411 pub pending_references: HashMap<String, Vec<VertexId>>,
1413}
1414
1415impl TombstoneRegistry {
1416 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1418 self.pending_references
1419 .entry(sheet_name)
1420 .or_default()
1421 .push(vertex_id);
1422 }
1423
1424 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1426 self.pending_references
1427 .remove(sheet_name)
1428 .unwrap_or_default()
1429 }
1430}