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::engine::template::dependency_summary::summarize_canonical_template;
161 use crate::engine::template::domain::{PlacementDomain, ResultRegion};
162 use crate::engine::template::read_summary::SpanReadSummary;
163 use crate::reference::{CellRef, Coord};
164 use crate::traits::EvaluationContext;
165 use formualizer_common::{ExcelError, LiteralValue};
166 use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, parse};
167 use std::collections::BTreeSet;
168 use std::sync::Arc;
169
170 #[derive(Clone, Debug)]
171 pub struct Fp8ParityObservation {
172 pub formula: String,
173 pub placement: CellRef,
174 pub old_payload: String,
175 pub new_hash: u64,
176 }
177
178 pub fn default_config() -> EvalConfig {
179 EvalConfig::default()
180 }
181
182 pub fn parse_formula(formula: &str) -> ASTNode {
183 parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"))
184 }
185
186 pub fn cell(sheet_id: u16, row: u32, col: u32) -> CellRef {
187 CellRef::new(sheet_id, Coord::from_excel(row, col, true, true))
188 }
189
190 fn local_binding_declarations(ast: &ASTNode, out: &mut BTreeSet<String>) {
191 match &ast.node_type {
192 ASTNodeType::Function { name, args } => {
193 let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
194 let declaration_indices: Box<dyn Iterator<Item = usize>> = match canonical.as_str()
195 {
196 "LET" => Box::new((0..args.len().saturating_sub(1)).step_by(2)),
197 "LAMBDA" => Box::new(0..args.len().saturating_sub(1)),
198 _ => Box::new(std::iter::empty()),
199 };
200 for index in declaration_indices {
201 if let Some(ASTNode {
202 node_type:
203 ASTNodeType::Reference {
204 reference: ReferenceType::NamedRange(name),
205 ..
206 },
207 ..
208 }) = args.get(index)
209 {
210 out.insert(name.to_ascii_uppercase());
211 }
212 }
213 for arg in args {
214 local_binding_declarations(arg, out);
215 }
216 }
217 ASTNodeType::UnaryOp { expr, .. } => local_binding_declarations(expr, out),
218 ASTNodeType::BinaryOp { left, right, .. } => {
219 local_binding_declarations(left, out);
220 local_binding_declarations(right, out);
221 }
222 ASTNodeType::Call { callee, args } => {
223 local_binding_declarations(callee, out);
224 for arg in args {
225 local_binding_declarations(arg, out);
226 }
227 }
228 ASTNodeType::Array(rows) => {
229 for item in rows.iter().flatten() {
230 local_binding_declarations(item, out);
231 }
232 }
233 _ => {}
234 }
235 }
236
237 pub fn assert_case<R: EvaluationContext>(
238 engine: &mut Engine<R>,
239 formula: &str,
240 placement: CellRef,
241 ) -> Fp8ParityObservation {
242 let parsed = parse_formula(formula);
243 assert_case_ast(engine, formula, parsed, placement)
244 }
245
246 pub fn assert_case_ast<R: EvaluationContext>(
247 engine: &mut Engine<R>,
248 formula: &str,
249 parsed: ASTNode,
250 placement: CellRef,
251 ) -> Fp8ParityObservation {
252 let mut local_declarations = BTreeSet::new();
253 local_binding_declarations(&parsed, &mut local_declarations);
254 let mut old_ast = parsed.clone();
255 let old_rewrite = engine
256 .graph
257 .rewrite_structured_references_for_cell(&mut old_ast, placement);
258 let old = old_rewrite.and_then(|_| old_path(engine, &old_ast, placement));
259
260 let new = {
261 let mut pipeline = engine.ingest_pipeline();
262 pipeline.ingest_formula(
263 crate::engine::ingest_pipeline::FormulaAstInput::Tree(parsed),
264 placement,
265 Some(Arc::<str>::from(formula)),
266 )
267 };
268
269 match (old, new) {
270 (Ok(old), Ok(new)) => {
271 let new_direct = sorted_cells(new.dep_plan.direct_cell_deps.clone());
272 assert_eq!(
273 old.direct_cells, new_direct,
274 "direct deps differ for {formula} at {placement:?}\nold={:?}\nnew={:?}",
275 old.direct_cells, new_direct
276 );
277 assert_eq!(
278 old.range_deps, new.dep_plan.range_deps,
279 "range deps differ for {formula} at {placement:?}"
280 );
281 let old_unresolved_names: Vec<_> = old
282 .unresolved_names
283 .iter()
284 .filter(|name| !local_declarations.contains(&name.to_ascii_uppercase()))
285 .cloned()
286 .collect();
287 assert_eq!(
288 old_unresolved_names, new.dep_plan.named_refs,
289 "unresolved names differ for {formula} at {placement:?}"
290 );
291 assert_eq!(
292 old.volatile, new.dep_plan.volatile,
293 "volatile flag differs for {formula} at {placement:?}"
294 );
295 assert_eq!(
296 old.dynamic, new.dep_plan.dynamic,
297 "dynamic flag differs for {formula} at {placement:?}"
298 );
299 let mut expected_labels = canonical_labels_from_old(&old.labels);
300 if old.dynamic {
301 expected_labels.flags |= CanonicalLabels::FLAG_DYNAMIC;
302 }
303 assert_eq!(
304 expected_labels.flags, new.labels.flags,
305 "canonical label flags differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
306 old.labels.flags, new.labels.flags
307 );
308 assert_eq!(
309 expected_labels.rejects, new.labels.rejects,
310 "canonical label rejects differ for {formula} at {placement:?}\nold={:?}\nnew={:#x}",
311 old.labels.reject_reasons, new.labels.rejects
312 );
313 let named_resolution_superset = old.summary_rejected_only_for_named_reference
319 && old.read_summary_debug.is_none();
320 if !named_resolution_superset {
321 assert_eq!(
322 old.read_summary_debug,
323 new.read_summary.as_ref().map(|s| format!("{s:?}")),
324 "read summary differs for {formula} at {placement:?}"
325 );
326 }
327 assert_eq!(new.formula_text.as_deref(), Some(formula));
328 assert_eq!(new.placement, placement);
329 Fp8ParityObservation {
330 formula: formula.to_string(),
331 placement,
332 old_payload: old.payload,
333 new_hash: new.canonical_hash,
334 }
335 }
336 (Err(old), Err(new)) => {
337 assert_eq!(
338 old.kind.to_string(),
339 new.kind.to_string(),
340 "old and new errored differently for {formula} at {placement:?}: old={old:?} new={new:?}"
341 );
342 Fp8ParityObservation {
343 formula: formula.to_string(),
344 placement,
345 old_payload: format!("ERR:{:?}", old.kind),
346 new_hash: 0,
347 }
348 }
349 (Ok(_), Err(new)) => panic!(
350 "new pipeline errored but old path succeeded for {formula} at {placement:?}: {new:?}"
351 ),
352 (Err(old), Ok(_)) => panic!(
353 "old path errored but new pipeline succeeded for {formula} at {placement:?}: {old:?}"
354 ),
355 }
356 }
357
358 #[derive(Debug)]
359 struct OldOutput {
360 payload: String,
361 labels: crate::engine::template::canonical::CanonicalTemplateLabels,
362 direct_cells: Vec<CellRef>,
363 range_deps: Vec<crate::reference::SharedRangeRef<'static>>,
364 unresolved_names: Vec<String>,
365 volatile: bool,
366 dynamic: bool,
367 read_summary_debug: Option<String>,
368 summary_rejected_only_for_named_reference: bool,
369 }
370
371 fn old_path<R: EvaluationContext>(
372 engine: &mut Engine<R>,
373 ast: &ASTNode,
374 placement: CellRef,
375 ) -> Result<OldOutput, ExcelError> {
376 let (_deps, ranges, placeholders, _named, unresolved_names) = engine
377 .graph
378 .fp8_parity_extract_dependencies_with_pending_names(ast, placement.sheet_id)?;
379 let volatile = engine.graph.fp8_parity_is_ast_volatile(ast);
380 let dynamic = engine.graph.is_ast_dynamic(ast);
381 let template =
382 canonicalize_template(ast, placement.coord.row() + 1, placement.coord.col() + 1);
383 let summary = summarize_canonical_template(&template);
384 let scalar_domain = PlacementDomain::row_run(
385 placement.sheet_id,
386 placement.coord.row(),
387 placement.coord.row(),
388 placement.coord.col(),
389 );
390 let result_region = ResultRegion::scalar_cells(scalar_domain);
391 let read_summary = SpanReadSummary::from_formula_summary(
392 placement.sheet_id,
393 &result_region,
394 &summary,
395 engine.graph.sheet_reg(),
396 )
397 .ok();
398 let summary_rejected_only_for_named_reference = !summary.reject_reasons.is_empty()
399 && summary.reject_reasons.iter().all(|reason| {
400 matches!(
401 reason,
402 crate::engine::template::dependency_summary::DependencyRejectReason
403 ::NamedRangeUnsupported { .. }
404 )
405 });
406 Ok(OldOutput {
407 payload: template.key.payload().to_string(),
408 labels: template.labels,
409 direct_cells: sorted_cells(placeholders),
410 range_deps: ranges,
411 unresolved_names,
412 volatile,
413 dynamic,
414 read_summary_debug: read_summary.as_ref().map(|s| format!("{s:?}")),
415 summary_rejected_only_for_named_reference,
416 })
417 }
418
419 fn sorted_cells(mut cells: Vec<CellRef>) -> Vec<CellRef> {
420 cells.sort();
421 cells.dedup();
422 cells
423 }
424
425 fn canonical_labels_from_old(
426 old: &crate::engine::template::canonical::CanonicalTemplateLabels,
427 ) -> CanonicalLabels {
428 let mut labels = CanonicalLabels::default();
429 for flag in &old.flags {
430 labels.flags |= match flag {
431 CanonicalTemplateFlag::ParserVolatileFlag => CanonicalLabels::FLAG_VOLATILE,
432 CanonicalTemplateFlag::FunctionCall => CanonicalLabels::FLAG_CONTAINS_FUNCTION,
433 CanonicalTemplateFlag::CurrentSheetBinding => CanonicalLabels::FLAG_CURRENT_SHEET,
434 CanonicalTemplateFlag::ExplicitSheetBinding => CanonicalLabels::FLAG_EXPLICIT_SHEET,
435 CanonicalTemplateFlag::RelativeReferenceAxis => CanonicalLabels::FLAG_RELATIVE_ONLY,
436 CanonicalTemplateFlag::AbsoluteReferenceAxis => CanonicalLabels::FLAG_ABSOLUTE_ONLY,
437 CanonicalTemplateFlag::MixedAnchors => CanonicalLabels::FLAG_MIXED_ANCHORS,
438 CanonicalTemplateFlag::FiniteRangeReference => CanonicalLabels::FLAG_CONTAINS_RANGE,
439 CanonicalTemplateFlag::NamedReference => CanonicalLabels::FLAG_CONTAINS_NAME,
440 };
441 }
442 for reason in &old.reject_reasons {
443 labels.flags |= match reason {
444 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
445 CanonicalLabels::FLAG_DYNAMIC
446 }
447 CanonicalRejectReason::ParserVolatileFlag
448 | CanonicalRejectReason::VolatileFunction { .. } => CanonicalLabels::FLAG_VOLATILE,
449 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
450 CanonicalLabels::FLAG_CONTAINS_LET_LAMBDA
451 }
452 CanonicalRejectReason::ArrayOrSpillFunction { .. }
453 | CanonicalRejectReason::ArrayLiteral => CanonicalLabels::FLAG_CONTAINS_ARRAY,
454 CanonicalRejectReason::StructuredReference { .. }
455 | CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
456 CanonicalLabels::FLAG_CONTAINS_TABLE
457 | CanonicalLabels::FLAG_CONTAINS_STRUCTURED_REF
458 }
459 CanonicalRejectReason::OpenRangeReference { .. }
460 | CanonicalRejectReason::WholeAxisReference { .. } => {
461 CanonicalLabels::FLAG_CONTAINS_RANGE
462 }
463 _ => 0,
464 };
465 labels.rejects |= match reason {
466 CanonicalRejectReason::InvalidPlacementAnchor { .. } => {
467 CanonicalLabels::REJECT_INVALID_PLACEMENT_ANCHOR
468 }
469 CanonicalRejectReason::DynamicReferenceFunction { .. } => {
470 CanonicalLabels::REJECT_DYNAMIC_REFERENCE
471 }
472 CanonicalRejectReason::UnknownOrCustomFunction { .. } => {
473 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
474 }
475 CanonicalRejectReason::LocalEnvironmentFunction { .. } => {
476 CanonicalLabels::REJECT_LOCAL_ENVIRONMENT
477 }
478 CanonicalRejectReason::ParserVolatileFlag => {
479 CanonicalLabels::REJECT_PARSER_VOLATILE_FLAG
480 }
481 CanonicalRejectReason::VolatileFunction { .. } => {
482 CanonicalLabels::REJECT_VOLATILE_FUNCTION
483 }
484 CanonicalRejectReason::ReferenceReturningFunction { .. } => {
485 CanonicalLabels::REJECT_REFERENCE_RETURNING_FUNCTION
486 }
487 CanonicalRejectReason::ArrayOrSpillFunction { .. } => {
488 CanonicalLabels::REJECT_ARRAY_OR_SPILL_FUNCTION
489 }
490 CanonicalRejectReason::ArrayLiteral => CanonicalLabels::REJECT_ARRAY_LITERAL,
491 CanonicalRejectReason::SpillReference { .. } => {
492 CanonicalLabels::REJECT_SPILL_REFERENCE
493 }
494 CanonicalRejectReason::SpillResultRegionOperator => {
495 CanonicalLabels::REJECT_SPILL_RESULT_REGION_OPERATOR
496 }
497 CanonicalRejectReason::ImplicitIntersectionOperator => {
498 CanonicalLabels::REJECT_IMPLICIT_INTERSECTION_OPERATOR
499 }
500 CanonicalRejectReason::CallExpression => CanonicalLabels::REJECT_CALL_EXPRESSION,
501 CanonicalRejectReason::StructuredReference { .. } => {
502 CanonicalLabels::REJECT_STRUCTURED_REFERENCE
503 }
504 CanonicalRejectReason::StructuredReferenceCurrentRow { .. } => {
505 CanonicalLabels::REJECT_STRUCTURED_REFERENCE_CURRENT_ROW
506 }
507 CanonicalRejectReason::ThreeDReference { .. } => {
508 CanonicalLabels::REJECT_THREE_D_REFERENCE
509 }
510 CanonicalRejectReason::ExternalReference { .. } => {
511 CanonicalLabels::REJECT_EXTERNAL_REFERENCE
512 }
513 CanonicalRejectReason::OpenRangeReference { .. } => {
514 CanonicalLabels::REJECT_OPEN_RANGE_REFERENCE
515 }
516 CanonicalRejectReason::WholeAxisReference { .. } => {
517 CanonicalLabels::REJECT_WHOLE_AXIS_REFERENCE
518 }
519 CanonicalRejectReason::UnsupportedReference { .. } => {
520 CanonicalLabels::REJECT_UNSUPPORTED_REFERENCE
521 }
522 CanonicalRejectReason::FunctionContractUnsupported { .. }
523 | CanonicalRejectReason::ContextDependentFunction { .. } => {
524 CanonicalLabels::REJECT_UNKNOWN_OR_CUSTOM_FUNCTION
525 }
526 };
527 }
528 labels
529 }
530
531 pub fn literal_number(value: f64) -> LiteralValue {
532 LiteralValue::Number(value)
533 }
534}
535
536use crate::timezone::TimeZoneSpec;
539use crate::traits::EvaluationContext;
540use crate::traits::VolatileLevel;
541use chrono::{DateTime, Utc};
542use formualizer_common::error::{ExcelError, ExcelErrorKind};
543use std::collections::HashMap;
544
545impl<R: EvaluationContext> Engine<R> {
546 pub fn begin_bulk_ingest(&mut self) -> ingest_builder::BulkIngestBuilder<'_> {
547 ingest_builder::BulkIngestBuilder::new(&mut self.graph)
548 }
549
550 pub fn intern_formula_ast(&mut self, ast: &formualizer_parse::parser::ASTNode) -> AstNodeId {
551 self.graph.store_ast(ast)
552 }
553
554 pub fn stage_formula_ast(
563 &mut self,
564 grouper: &mut FormulaFamilyGrouper,
565 row: u32,
566 col: u32,
567 ast: &formualizer_parse::parser::ASTNode,
568 formula_text: Option<std::sync::Arc<str>>,
569 ) -> FormulaIngestRecord {
570 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
571 if self.config.formula_compression
572 && let Some(family) = self.graph.group_formula_member(grouper, row0, col0, ast)
573 {
574 let record = FormulaIngestRecord::member(row, col, family.template, family.anchor);
575 grouper.members += 1;
576 grouper.counters.parsed_members += 1;
577 grouper.note(row0, col0, family);
578 return record;
579 }
580 let ast_id = self.intern_formula_ast(ast);
581 self.note_staged_formula(grouper, row, col, ast_id);
582 FormulaIngestRecord::new(row, col, ast_id, formula_text)
583 }
584
585 #[doc(hidden)]
588 pub fn source_family_mode(&self) -> SourceFamilyMode {
589 self.source_family_mode
590 }
591
592 #[doc(hidden)]
593 pub fn set_source_family_mode(&mut self, mode: SourceFamilyMode) {
594 self.source_family_mode = mode;
595 }
596
597 #[doc(hidden)]
599 pub fn source_family_counters(&self) -> SourceFamilyCounters {
600 self.source_family_counters
601 }
602
603 #[doc(hidden)]
606 pub fn finish_family_grouper(&mut self, grouper: &mut FormulaFamilyGrouper) {
607 let counters = std::mem::take(&mut grouper.counters);
608 counters.publish();
609 self.source_family_counters.accumulate(&counters);
610 }
611
612 pub fn note_staged_formula(
615 &mut self,
616 grouper: &mut FormulaFamilyGrouper,
617 row: u32,
618 col: u32,
619 ast_id: AstNodeId,
620 ) {
621 let (row0, col0) = (row.saturating_sub(1), col.saturating_sub(1));
622 grouper.note(
623 row0,
624 col0,
625 formula_ingest::GroupedFamily {
626 template: ast_id,
627 anchor: (row0, col0),
628 rendered: None,
629 lexical: None,
630 },
631 );
632 }
633}
634
635pub trait CalcObserver: Send + Sync {
637 fn on_eval_start(&self, vertex_id: VertexId);
638 fn on_eval_complete(&self, vertex_id: VertexId, duration: std::time::Duration);
639 fn on_cycle_detected(&self, cycle: &[VertexId]);
640 fn on_dirty_propagation(&self, vertex_id: VertexId, affected_count: usize);
641}
642
643impl CalcObserver for () {
645 fn on_eval_start(&self, _vertex_id: VertexId) {}
646 fn on_eval_complete(&self, _vertex_id: VertexId, _duration: std::time::Duration) {}
647 fn on_cycle_detected(&self, _cycle: &[VertexId]) {}
648 fn on_dirty_propagation(&self, _vertex_id: VertexId, _affected_count: usize) {}
649}
650
651#[derive(Debug, Clone, PartialEq, Eq)]
655pub enum DeterministicMode {
656 Disabled {
658 timezone: TimeZoneSpec,
660 },
661 Enabled {
663 timestamp_utc: DateTime<Utc>,
665 timezone: TimeZoneSpec,
667 },
668}
669
670impl Default for DeterministicMode {
671 fn default() -> Self {
672 Self::Disabled {
673 timezone: TimeZoneSpec::default(),
674 }
675 }
676}
677
678impl DeterministicMode {
679 pub fn is_enabled(&self) -> bool {
680 matches!(self, DeterministicMode::Enabled { .. })
681 }
682
683 pub fn timezone(&self) -> &TimeZoneSpec {
684 match self {
685 DeterministicMode::Disabled { timezone } => timezone,
686 DeterministicMode::Enabled { timezone, .. } => timezone,
687 }
688 }
689
690 pub fn validate(&self) -> Result<(), ExcelError> {
691 if let DeterministicMode::Enabled { timezone, .. } = self {
692 timezone
693 .validate_for_determinism()
694 .map_err(|msg| ExcelError::new(ExcelErrorKind::Value).with_message(msg))?;
695 }
696 Ok(())
697 }
698
699 pub fn build_clock(
700 &self,
701 ) -> Result<std::sync::Arc<dyn crate::timezone::ClockProvider>, ExcelError> {
702 self.validate()?;
703 Ok(match self {
704 #[cfg(feature = "system-clock")]
705 DeterministicMode::Disabled { timezone } => {
706 std::sync::Arc::new(crate::timezone::SystemClock::new(timezone.clone()))
707 }
708 #[cfg(not(feature = "system-clock"))]
709 DeterministicMode::Disabled { timezone: _ } => {
710 std::sync::Arc::new(crate::timezone::FixedClock::new(
715 chrono::DateTime::UNIX_EPOCH,
716 crate::timezone::TimeZoneSpec::Utc,
717 ))
718 }
719 DeterministicMode::Enabled {
720 timestamp_utc,
721 timezone,
722 } => std::sync::Arc::new(crate::timezone::FixedClock::new(
723 *timestamp_utc,
724 timezone.clone(),
725 )),
726 })
727 }
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732pub enum FormulaParsePolicy {
733 Strict,
735 CoerceToError,
737 KeepCachedValue,
739 AsText,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq)]
745pub struct FormulaParseDiagnostic {
746 pub sheet: String,
747 pub row: u32,
748 pub col: u32,
749 pub formula: String,
750 pub message: String,
751 pub policy: FormulaParsePolicy,
752}
753
754#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
755pub enum FormulaPlaneMode {
756 #[default]
758 Off,
759 Shadow,
761 AuthoritativeExperimental,
763}
764
765#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767pub enum FormulaSpoolDiskPolicy {
768 NativeSpill,
770 MemoryOnly,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
776pub struct WorkbookLoadLimits {
777 pub max_sheet_rows: u32,
779 pub max_sheet_cols: u32,
781 pub max_sheet_logical_cells: u64,
783 pub max_formula_plane_fallback_cells: u64,
786 pub sparse_sheet_cell_threshold: u64,
788 pub max_sparse_cell_ratio: u64,
790 pub max_formula_spool_bytes_per_sheet: u64,
792 pub max_formula_spool_bytes_per_workbook: u64,
794 pub max_formula_spool_files_per_workbook: u32,
796 pub formula_spool_memory_prefix_bytes: u64,
798 pub max_formula_spool_memory_bytes: u64,
800 pub formula_spool_disk_policy: FormulaSpoolDiskPolicy,
802}
803
804impl Default for WorkbookLoadLimits {
805 fn default() -> Self {
806 Self {
807 max_sheet_rows: 1_048_576,
808 max_sheet_cols: 16_384,
809 max_sheet_logical_cells: 128_000_000,
810 max_formula_plane_fallback_cells: 2_000_000,
811 sparse_sheet_cell_threshold: 250_000,
812 max_sparse_cell_ratio: 1_024,
813 max_formula_spool_bytes_per_sheet: 256 * 1024 * 1024,
814 max_formula_spool_bytes_per_workbook: 1024 * 1024 * 1024,
815 max_formula_spool_files_per_workbook: 1_024,
816 formula_spool_memory_prefix_bytes: 1024 * 1024,
817 max_formula_spool_memory_bytes: 16 * 1024 * 1024,
818 formula_spool_disk_policy: if cfg!(target_arch = "wasm32") {
819 FormulaSpoolDiskPolicy::MemoryOnly
820 } else {
821 FormulaSpoolDiskPolicy::NativeSpill
822 },
823 }
824 }
825}
826
827#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
829pub enum TemporalEgress {
830 #[default]
831 Native,
832 Serial,
833}
834
835#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
838pub enum PreparationPolicy {
839 Strict,
842 #[default]
846 BestEffort,
847}
848
849#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
862#[non_exhaustive]
863pub enum NumericProfile {
864 #[default]
868 ExcelCompatible,
869}
870
871#[derive(Debug, Clone)]
873pub struct EvalConfig {
874 pub enable_parallel: bool,
875 pub max_threads: Option<usize>,
876 pub max_vertices: Option<usize>,
879 pub max_eval_time: Option<std::time::Duration>,
882 pub max_memory_mb: Option<usize>,
886 pub evaluation_budgets: EvaluationBudgets,
890
891 pub default_sheet_name: String,
893
894 pub case_sensitive_names: bool,
898
899 pub case_sensitive_tables: bool,
903
904 pub workbook_seed: u64,
906
907 pub volatile_level: VolatileLevel,
909
910 pub deterministic_mode: DeterministicMode,
912
913 pub range_expansion_limit: usize,
916
917 pub max_open_ended_rows: u32,
921
922 pub max_open_ended_cols: u32,
926
927 pub stripe_height: u32,
929 pub stripe_width: u32,
931 pub enable_block_stripes: bool,
935
936 pub spill: SpillConfig,
938
939 pub cycle: CycleConfig,
943
944 pub use_dynamic_topo: bool,
949 pub pk_visit_budget: usize,
951 pub pk_compaction_interval_ops: u64,
953 pub max_layer_width: Option<usize>,
955 pub pk_reject_cycle_edges: bool,
958 pub sheet_index_mode: SheetIndexMode,
960
961 pub warmup: tuning::WarmupConfig,
963
964 pub arrow_storage_enabled: bool,
966 pub delta_overlay_enabled: bool,
968
969 pub write_formula_overlay_enabled: bool,
972
973 pub max_overlay_memory_bytes: Option<usize>,
978
979 pub date_system: DateSystem,
981
982 pub temporal_egress: TemporalEgress,
984
985 pub formula_parse_policy: FormulaParsePolicy,
987
988 pub defer_graph_building: bool,
991
992 pub preparation_policy: PreparationPolicy,
995
996 pub enable_virtual_dep_telemetry: bool,
1000
1001 pub formula_plane_mode: FormulaPlaneMode,
1005 pub max_formula_plane_cache_candidates: usize,
1007 pub max_formula_plane_cache_edges: usize,
1009 pub max_formula_plane_cache_bytes: usize,
1011
1012 pub lookup_index_cache_max_bytes: usize,
1014
1015 pub family_execution: bool,
1020
1021 pub family_kernels: bool,
1025
1026 pub family_lift: bool,
1031
1032 pub formula_compression: bool,
1038
1039 pub numeric_profile: NumericProfile,
1041}
1042
1043impl Default for EvalConfig {
1044 fn default() -> Self {
1045 Self {
1046 enable_parallel: true,
1047 max_threads: None,
1048 max_vertices: None,
1049 max_eval_time: None,
1050 max_memory_mb: None,
1051 evaluation_budgets: EvaluationBudgets::default(),
1052
1053 default_sheet_name: format!("Sheet{}", 1),
1054
1055 case_sensitive_names: false,
1057 case_sensitive_tables: false,
1058
1059 workbook_seed: 0xF0F0_D0D0_AAAA_5555,
1061
1062 volatile_level: VolatileLevel::Always,
1064
1065 deterministic_mode: DeterministicMode::default(),
1066
1067 range_expansion_limit: 64,
1069 max_open_ended_rows: 1_048_576,
1072 max_open_ended_cols: 16_384,
1073 stripe_height: 256,
1074 stripe_width: 256,
1075 enable_block_stripes: false,
1076 spill: SpillConfig::default(),
1077 cycle: CycleConfig::default(),
1078
1079 use_dynamic_topo: false, pk_visit_budget: 50_000,
1082 pk_compaction_interval_ops: 100_000,
1083 max_layer_width: None,
1084 pk_reject_cycle_edges: false,
1085 sheet_index_mode: SheetIndexMode::Eager,
1086 warmup: tuning::WarmupConfig::default(),
1087 arrow_storage_enabled: true,
1088 delta_overlay_enabled: true,
1089 write_formula_overlay_enabled: true,
1090 max_overlay_memory_bytes: None,
1091 date_system: DateSystem::Excel1900,
1092 temporal_egress: TemporalEgress::default(),
1093 formula_parse_policy: FormulaParsePolicy::Strict,
1094 defer_graph_building: false,
1095 preparation_policy: PreparationPolicy::BestEffort,
1096 enable_virtual_dep_telemetry: false,
1097 formula_plane_mode: FormulaPlaneMode::Off,
1098 max_formula_plane_cache_candidates: 100_000,
1099 max_formula_plane_cache_edges: 100_000,
1100 max_formula_plane_cache_bytes: 64 * 1024 * 1024,
1101 lookup_index_cache_max_bytes: 64 * 1024 * 1024,
1102 family_execution: true,
1103 family_kernels: true,
1104 family_lift: true,
1105 formula_compression: true,
1106 numeric_profile: NumericProfile::ExcelCompatible,
1107 }
1108 }
1109}
1110
1111impl EvalConfig {
1112 #[inline]
1113 pub fn with_preparation_policy(mut self, policy: PreparationPolicy) -> Self {
1114 self.preparation_policy = policy;
1115 self
1116 }
1117
1118 pub fn with_range_expansion_limit(mut self, limit: usize) -> Self {
1119 self.range_expansion_limit = limit;
1120 self
1121 }
1122
1123 #[inline]
1124 pub fn with_parallel(mut self, enable: bool) -> Self {
1125 self.enable_parallel = enable;
1126 self
1127 }
1128
1129 #[inline]
1130 pub fn with_block_stripes(mut self, enable: bool) -> Self {
1131 self.enable_block_stripes = enable;
1132 self
1133 }
1134
1135 #[inline]
1136 pub fn with_case_sensitive_names(mut self, enable: bool) -> Self {
1137 self.case_sensitive_names = enable;
1138 self
1139 }
1140
1141 #[inline]
1142 pub fn with_case_sensitive_tables(mut self, enable: bool) -> Self {
1143 self.case_sensitive_tables = enable;
1144 self
1145 }
1146
1147 #[inline]
1148 pub fn with_arrow_storage(mut self, enable: bool) -> Self {
1149 self.arrow_storage_enabled = enable;
1150 self
1151 }
1152
1153 #[inline]
1154 pub fn with_delta_overlay(mut self, enable: bool) -> Self {
1155 self.delta_overlay_enabled = enable;
1156 self
1157 }
1158
1159 #[inline]
1160 pub fn with_formula_overlay(mut self, enable: bool) -> Self {
1161 self.write_formula_overlay_enabled = enable;
1162 self
1163 }
1164
1165 #[inline]
1166 pub fn with_date_system(mut self, system: DateSystem) -> Self {
1167 self.date_system = system;
1168 self
1169 }
1170
1171 #[inline]
1172 pub fn with_formula_parse_policy(mut self, policy: FormulaParsePolicy) -> Self {
1173 self.formula_parse_policy = policy;
1174 self
1175 }
1176
1177 #[inline]
1178 pub fn with_virtual_dep_telemetry(mut self, enable: bool) -> Self {
1179 self.enable_virtual_dep_telemetry = enable;
1180 self
1181 }
1182
1183 #[inline]
1184 pub fn with_formula_plane_mode(mut self, mode: FormulaPlaneMode) -> Self {
1185 self.formula_plane_mode = mode;
1186 self
1187 }
1188
1189 #[inline]
1190 pub fn with_evaluation_budgets(mut self, budgets: EvaluationBudgets) -> Self {
1191 self.evaluation_budgets = budgets;
1192 self
1193 }
1194
1195 pub fn resolved_evaluation_budgets(&self) -> EvaluationBudgets {
1197 resource_ledger::resolve_evaluation_budgets(
1198 &self.evaluation_budgets,
1199 self.max_vertices,
1200 self.max_memory_mb,
1201 self.max_eval_time,
1202 )
1203 .budgets
1204 }
1205
1206 #[inline]
1215 pub fn with_cycle(mut self, cycle: CycleConfig) -> Self {
1216 if let Err(msg) = cycle.validate() {
1217 panic!("invalid CycleConfig: {msg}");
1218 }
1219 self.cycle = cycle;
1220 self
1221 }
1222}
1223
1224#[derive(Debug, Clone, Copy, PartialEq, Default)]
1229pub struct CycleConfig {
1230 pub detection: CycleDetection,
1231 pub policy: CyclePolicy,
1232}
1233
1234impl CycleConfig {
1235 pub fn iterate_excel_defaults() -> Self {
1238 Self {
1239 detection: CycleDetection::Runtime,
1240 policy: CyclePolicy::iterate_excel_defaults(),
1241 }
1242 }
1243
1244 pub fn iterate(max_iterations: u32, max_change: f64) -> Self {
1246 Self {
1247 detection: CycleDetection::Runtime,
1248 policy: CyclePolicy::Iterate {
1249 max_iterations,
1250 max_change,
1251 },
1252 }
1253 }
1254
1255 pub fn validate(&self) -> Result<(), String> {
1259 if let CyclePolicy::Iterate {
1260 max_iterations,
1261 max_change,
1262 } = self.policy
1263 {
1264 if self.detection == CycleDetection::Static {
1265 return Err(
1266 "CyclePolicy::Iterate requires CycleDetection::Runtime (spec §2)".to_string(),
1267 );
1268 }
1269 if max_iterations == 0 {
1270 return Err("CyclePolicy::Iterate max_iterations must be >= 1".to_string());
1271 }
1272 if !max_change.is_finite() || max_change < 0.0 {
1273 return Err(format!(
1274 "CyclePolicy::Iterate max_change must be finite and >= 0 (got {max_change})"
1275 ));
1276 }
1277 }
1278 Ok(())
1279 }
1280
1281 #[inline]
1286 pub(crate) fn allows_self_dependency(&self) -> bool {
1287 self.detection == CycleDetection::Runtime
1288 && matches!(self.policy, CyclePolicy::Iterate { .. })
1289 }
1290}
1291
1292#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1294pub enum CycleDetection {
1295 #[default]
1298 Static,
1299 Runtime,
1303}
1304
1305#[derive(Debug, Clone, Copy, PartialEq, Default)]
1307pub enum CyclePolicy {
1308 #[default]
1310 Error,
1311 Iterate {
1319 max_iterations: u32,
1323 max_change: f64,
1327 },
1328}
1329
1330impl CyclePolicy {
1331 pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1333 pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1335
1336 pub fn iterate_excel_defaults() -> Self {
1338 CyclePolicy::Iterate {
1339 max_iterations: Self::EXCEL_DEFAULT_MAX_ITERATIONS,
1340 max_change: Self::EXCEL_DEFAULT_MAX_CHANGE,
1341 }
1342 }
1343}
1344
1345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1346pub enum SheetIndexMode {
1347 Eager,
1349 Lazy,
1351 FastBatch,
1353}
1354
1355pub use formualizer_common::DateSystem;
1356
1357pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1359where
1360 R: EvaluationContext + 'static,
1361{
1362 Engine::new(resolver, config)
1363}
1364
1365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1367pub struct SpillConfig {
1368 pub conflict_policy: SpillConflictPolicy,
1370 pub tiebreaker: SpillTiebreaker,
1372 pub bounds_policy: SpillBoundsPolicy,
1374 pub buffer_mode: SpillBufferMode,
1376 pub memory_budget_bytes: Option<u64>,
1378 pub cancellation: SpillCancellationPolicy,
1380 pub visibility: SpillVisibility,
1382
1383 pub max_spill_cells: u32,
1387}
1388
1389impl Default for SpillConfig {
1390 fn default() -> Self {
1391 Self {
1392 conflict_policy: SpillConflictPolicy::Error,
1393 tiebreaker: SpillTiebreaker::FirstWins,
1394 bounds_policy: SpillBoundsPolicy::Strict,
1395 buffer_mode: SpillBufferMode::ShadowBuffer,
1396 memory_budget_bytes: None,
1397 cancellation: SpillCancellationPolicy::Cooperative,
1398 visibility: SpillVisibility::OnCommit,
1399 max_spill_cells: 10_000,
1401 }
1402 }
1403}
1404
1405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1406pub enum SpillConflictPolicy {
1407 Error,
1408 Preempt,
1409}
1410
1411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1412pub enum SpillTiebreaker {
1413 FirstWins,
1414 EvaluationEpochAsc,
1415 AnchorAddressAsc,
1416 FunctionPriorityThenAddress,
1417}
1418
1419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1420pub enum SpillBoundsPolicy {
1421 Strict,
1422 Truncate,
1423}
1424
1425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1426pub enum SpillBufferMode {
1427 ShadowBuffer,
1428 PersistenceJournal,
1429}
1430
1431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1432pub enum SpillCancellationPolicy {
1433 Cooperative,
1434 Strict,
1435}
1436
1437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1438pub enum SpillVisibility {
1439 OnCommit,
1440 StagedLayer,
1441}
1442
1443#[derive(Debug, Default)]
1454pub struct TombstoneRegistry {
1455 pub pending_references: HashMap<String, Vec<VertexId>>,
1457}
1458
1459impl TombstoneRegistry {
1460 pub fn add_orphan(&mut self, sheet_name: String, vertex_id: VertexId) {
1462 self.pending_references
1463 .entry(sheet_name)
1464 .or_default()
1465 .push(vertex_id);
1466 }
1467
1468 pub fn take_orphans(&mut self, sheet_name: &str) -> Vec<VertexId> {
1470 self.pending_references
1471 .remove(sheet_name)
1472 .unwrap_or_default()
1473 }
1474}