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