Skip to main content

formualizer_eval/engine/
mod.rs

1//! Formualizer Dependency Graph Engine
2//!
3//! Provides incremental formula evaluation with dependency tracking.
4
5pub 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
40// New SoA modules
41pub 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
51// Phase 1: Arena modules
52pub mod arena;
53
54// Phase 1: Warmup configuration (kept for compatibility)
55pub 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;
85// Use SoA implementation
86pub 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};
99// Internal accounting mechanism: reachable inside the crate, deliberately not
100// part of the published surface. See the type's documentation.
101pub(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                // The passive summary oracle cannot resolve defined names, so
286                // a named formula that the ingest pipeline resolved to a
287                // concrete region is an intentional superset: old None / new
288                // Some is allowed exactly when the only blocking reason was a
289                // named reference.
290                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
508// CalcObserver is defined below
509
510use 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
527/// 🔮 Scalability Hook: Performance monitoring trait for calculation observability
528pub 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
535/// Default no-op observer
536impl 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/// Deterministic evaluation configuration.
544///
545/// When enabled, volatile sources (clock/timezone) are derived solely from this config.
546#[derive(Debug, Clone, PartialEq, Eq)]
547pub enum DeterministicMode {
548    /// Non-deterministic: uses the system clock.
549    Disabled {
550        /// Timezone used by volatile date/time builtins.
551        timezone: TimeZoneSpec,
552    },
553    /// Deterministic: uses a fixed timestamp in the provided timezone.
554    Enabled {
555        /// Fixed timestamp expressed in UTC.
556        timestamp_utc: DateTime<Utc>,
557        /// Timezone used to interpret `timestamp_utc` for NOW()/TODAY().
558        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                // Without the system-clock feature, Disabled mode falls back to a
603                // UTC epoch clock so the engine still initialises cleanly in portable
604                // wasm guests.  Callers that need real wall-clock time must inject a
605                // `ClockProvider` via `EvalConfig::clock`.
606                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/// Policy for handling malformed formulas encountered during workbook ingest.
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum FormulaParsePolicy {
625    /// Reject malformed formulas and fail the load/evaluation path.
626    Strict,
627    /// Convert malformed formulas into literal error formulas (`#ERROR!`).
628    CoerceToError,
629    /// Keep the backend-provided cached value and drop the formula.
630    KeepCachedValue,
631    /// Treat the original formula text as a plain text literal.
632    AsText,
633}
634
635/// Captured diagnostic for a malformed formula encountered during ingest/graph-build.
636#[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    /// Disable FormulaPlane promotion/evaluation. This is the stable default;
649    /// span evaluation is explicitly opt-in through configuration.
650    #[default]
651    Off,
652    Shadow,
653    /// Experimental mode: accepted FormulaPlane spans are installed into
654    /// graph-owned authority and are not materialized as per-cell graph formulas.
655    AuthoritativeExperimental,
656}
657
658/// Storage policy for the private formula replay spool used while loading workbooks.
659#[derive(Debug, Clone, Copy, PartialEq, Eq)]
660pub enum FormulaSpoolDiskPolicy {
661    /// Spill to secure temporary storage after the configured memory prefix.
662    NativeSpill,
663    /// Never write formula replay data to a filesystem.
664    MemoryOnly,
665}
666
667/// Workbook ingest limits applied by loader backends before they materialize large sheets.
668#[derive(Debug, Clone, PartialEq, Eq)]
669pub struct WorkbookLoadLimits {
670    /// Hard cap for declared/logical sheet rows.
671    pub max_sheet_rows: u32,
672    /// Hard cap for declared/logical sheet columns.
673    pub max_sheet_cols: u32,
674    /// Hard cap for the rectangular logical area a backend may materialize.
675    pub max_sheet_logical_cells: u64,
676    /// Hard cap for formulas materialized by one FormulaPlane fallback.
677    pub max_formula_plane_fallback_cells: u64,
678    /// Sparse-sheet checks only trigger once a sheet reaches this many logical cells.
679    pub sparse_sheet_cell_threshold: u64,
680    /// Maximum allowed logical-to-populated-cell ratio once the sparse threshold is crossed.
681    pub max_sparse_cell_ratio: u64,
682    /// Maximum encoded formula replay bytes retained for one sheet.
683    pub max_formula_spool_bytes_per_sheet: u64,
684    /// Maximum encoded formula replay bytes produced across one workbook load.
685    pub max_formula_spool_bytes_per_workbook: u64,
686    /// Maximum number of native spill files created across one workbook load.
687    pub max_formula_spool_files_per_workbook: u32,
688    /// Encoded bytes retained in memory before native spill.
689    pub formula_spool_memory_prefix_bytes: u64,
690    /// Independent cap for a memory-only formula replay spool.
691    pub max_formula_spool_memory_bytes: u64,
692    /// Whether formula replay data may use secure native temporary storage.
693    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/// Controls whether temporal-formatted serials leave the engine as native values.
720#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
721pub enum TemporalEgress {
722    #[default]
723    Native,
724    Serial,
725}
726
727/// Configuration for the evaluation engine
728#[derive(Debug, Clone)]
729pub struct EvalConfig {
730    pub enable_parallel: bool,
731    pub max_threads: Option<usize>,
732    /// Deprecated. Maps to `evaluation_budgets.admission.graph_vertex_hard_limit` only when that
733    /// explicit field is unset.
734    pub max_vertices: Option<usize>,
735    /// Deprecated. Maps to `evaluation_budgets.deadline.max_elapsed` only when that explicit field
736    /// is unset.
737    pub max_eval_time: Option<std::time::Duration>,
738    /// Deprecated. Converts MiB to bytes and splits the result 50/50 between otherwise-unset
739    /// retained and scratch totals; an odd byte goes to retained. Each explicit total wins its own
740    /// conflict independently.
741    pub max_memory_mb: Option<usize>,
742    /// Explicit evaluation budgets. All fields are unset by default, preserving current behavior.
743    /// Deprecated resource fields fill only otherwise-unset destination fields and produce one
744    /// field-level diagnostic describing every mapping or conflict.
745    pub evaluation_budgets: EvaluationBudgets,
746
747    /// Default sheet name used when no sheet is provided.
748    pub default_sheet_name: String,
749
750    /// When false, resolve defined names case-insensitively (ASCII only).
751    ///
752    /// This matches Excel behavior for defined names.
753    pub case_sensitive_names: bool,
754
755    /// When false, resolve table names case-insensitively (ASCII only).
756    ///
757    /// This matches Excel behavior for native table (ListObject) names.
758    pub case_sensitive_tables: bool,
759
760    /// Stable workbook seed used for deterministic RNG composition
761    pub workbook_seed: u64,
762
763    /// Volatile granularity for RNG seeding and re-evaluation policy
764    pub volatile_level: VolatileLevel,
765
766    /// Deterministic evaluation configuration (clock/timezone injection).
767    pub deterministic_mode: DeterministicMode,
768
769    // Range handling configuration (Phase 5)
770    /// Ranges with size <= this limit are expanded into individual Cell dependencies
771    pub range_expansion_limit: usize,
772
773    /// Fallback maximum row bound for open-ended references (e.g. `A:A`, `A1:A`).
774    ///
775    /// This is only used when used-bounds cannot be determined.
776    pub max_open_ended_rows: u32,
777
778    /// Fallback maximum column bound for open-ended references (e.g. `1:1`, `A1:1`).
779    ///
780    /// This is only used when used-bounds cannot be determined.
781    pub max_open_ended_cols: u32,
782
783    /// Height of stripe blocks for dense range indexing
784    pub stripe_height: u32,
785    /// Width of stripe blocks for dense range indexing  
786    pub stripe_width: u32,
787    /// Enable block stripes for dense ranges (vs row/column stripes only)
788    pub enable_block_stripes: bool,
789
790    /// Spill behavior configuration (conflicts, bounds, buffering)
791    pub spill: SpillConfig,
792
793    /// Cycle handling configuration (detection mode + policy). Defaults to
794    /// `CycleDetection::Static` (today's stamp-every-static-SCC behavior);
795    /// `CycleDetection::Runtime` is opt-in (RFC #112).
796    pub cycle: CycleConfig,
797
798    /// Use dynamic topological ordering (Pearce-Kelly algorithm)
799    pub use_dynamic_topo: bool,
800    /// Maximum nodes to visit before falling back to full rebuild
801    pub pk_visit_budget: usize,
802    /// Operations between periodic rank compaction
803    pub pk_compaction_interval_ops: u64,
804    /// Maximum width for parallel evaluation layers
805    pub max_layer_width: Option<usize>,
806    /// If true, reject edge insertions that would create a cycle (skip adding that dependency).
807    /// If false, allow insertion and let scheduler handle cycles at evaluation time.
808    pub pk_reject_cycle_edges: bool,
809    /// Sheet index build strategy for bulk loads
810    pub sheet_index_mode: SheetIndexMode,
811
812    /// Warmup configuration for global pass planning (Phase 1)
813    pub warmup: tuning::WarmupConfig,
814
815    /// Enable Arrow-backed storage reads (Phase A)
816    pub arrow_storage_enabled: bool,
817    /// Enable delta overlay for Arrow sheets (Phase C)
818    pub delta_overlay_enabled: bool,
819
820    /// Mirror formula scalar results into Arrow overlay for Arrow-backed reads
821    /// This enables Arrow-only RangeView correctness without Hybrid fallback.
822    pub write_formula_overlay_enabled: bool,
823
824    /// Optional memory budget (in bytes) for formula/spill computed Arrow overlays.
825    ///
826    /// When set, the engine will compact computed overlays into base lanes when the
827    /// estimated usage exceeds this cap.
828    pub max_overlay_memory_bytes: Option<usize>,
829
830    /// Workbook date system: Excel 1900 (default) or 1904.
831    pub date_system: DateSystem,
832
833    /// Public temporal materialisation policy.
834    pub temporal_egress: TemporalEgress,
835
836    /// Policy for malformed formulas encountered during ingest/graph-build.
837    pub formula_parse_policy: FormulaParsePolicy,
838
839    /// Defer dependency graph building: ingest values immediately but stage formulas
840    /// for on-demand graph construction during evaluation.
841    pub defer_graph_building: bool,
842
843    /// Enable virtual dependency convergence telemetry collection.
844    ///
845    /// When disabled, the engine avoids per-pass timing/edge-count bookkeeping.
846    pub enable_virtual_dep_telemetry: bool,
847
848    /// FormulaPlane ingest/planning mode. Defaults to `Off`; span evaluation is
849    /// explicitly opt-in while `AuthoritativeExperimental` remains experimental.
850    /// `Shadow` may report candidate span opportunities but must still materialize
851    /// every formula via the legacy graph path.
852    pub formula_plane_mode: FormulaPlaneMode,
853    /// Hard candidate bound for compiling mixed FormulaPlane topology.
854    pub max_formula_plane_cache_candidates: usize,
855    /// Hard relationship bound for compiled mixed FormulaPlane topology.
856    pub max_formula_plane_cache_edges: usize,
857    /// Hard byte estimate bound for compiled mixed FormulaPlane topology.
858    pub max_formula_plane_cache_bytes: usize,
859
860    /// Maximum bytes for the engine-side lookup-index cache.
861    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            // Excel compatibility: identifiers are case-insensitive by default.
877            case_sensitive_names: false,
878            case_sensitive_tables: false,
879
880            // Deterministic RNG seed (matches traits default)
881            workbook_seed: 0xF0F0_D0D0_AAAA_5555,
882
883            // Volatile model default
884            volatile_level: VolatileLevel::Always,
885
886            deterministic_mode: DeterministicMode::default(),
887
888            // Range handling defaults (Phase 5)
889            range_expansion_limit: 64,
890            // Open-ended reference defaults (Excel max dimensions).
891            // Lower these to cap `A:A` / `1:1` when used-bounds are unknown.
892            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            // Dynamic topology configuration
901            use_dynamic_topo: false, // Disabled by default for compatibility
902            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    /// Resolve explicit and deprecated resource settings without consulting ambient host state.
1006    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    /// Set the cycle configuration.
1017    ///
1018    /// # Panics
1019    /// Panics when `cycle` is invalid (see [`CycleConfig::validate`]):
1020    /// `Iterate` with `detection: Static`, `max_iterations == 0`, or a
1021    /// negative/non-finite `max_change` are config errors rejected at build
1022    /// (spec §2). [`Engine::new`] re-validates for configs assembled via
1023    /// struct literals.
1024    #[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/// Cycle handling configuration (spec: `formualizer-cycle-semantics-spec.md` §2).
1035///
1036/// Nested under [`EvalConfig`] like [`SpillConfig`]; flows through
1037/// `WorkbookConfig.eval` automatically.
1038#[derive(Debug, Clone, Copy, PartialEq, Default)]
1039pub struct CycleConfig {
1040    pub detection: CycleDetection,
1041    pub policy: CyclePolicy,
1042}
1043
1044impl CycleConfig {
1045    /// Runtime detection + Excel-default iterative calculation
1046    /// (`max_iterations: 100`, `max_change: 0.001`).
1047    pub fn iterate_excel_defaults() -> Self {
1048        Self {
1049            detection: CycleDetection::Runtime,
1050            policy: CyclePolicy::iterate_excel_defaults(),
1051        }
1052    }
1053
1054    /// Runtime detection + iterative calculation with explicit knobs.
1055    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    /// Validate the configuration (spec §2). Invalid combinations are
1066    /// rejected at build: [`EvalConfig::with_cycle`] and engine construction
1067    /// both panic on `Err`.
1068    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    /// Whether ingest may accept formulas whose dependencies include the
1092    /// formula's own cell (`=B1+A1` in B1). Excel accepts these only with
1093    /// iterative calculation enabled; everywhere else the edit-time
1094    /// "Self-reference detected" rejection stands.
1095    #[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/// How statically-cyclic SCCs are treated at evaluation time.
1103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1104pub enum CycleDetection {
1105    /// Today's behavior: every static SCC is stamped `#CIRC!`. Compat escape
1106    /// hatch; no live-edge machinery runs.
1107    #[default]
1108    Static,
1109    /// Static SCCs are candidates; members are evaluated with live-edge
1110    /// recording and only *live* cycles get the policy verdict. Phantom
1111    /// (live-acyclic) SCCs produce ordinary values (discussion #99).
1112    Runtime,
1113}
1114
1115/// What happens to witnessed (live) cycles under `CycleDetection::Runtime`.
1116#[derive(Debug, Clone, Copy, PartialEq, Default)]
1117pub enum CyclePolicy {
1118    /// Live cycles produce `#CIRC!`.
1119    #[default]
1120    Error,
1121    /// Excel-style iterative calculation (RFC #113, spec §3.5/§6):
1122    /// live cycles keep running full passes over all SCC members in member
1123    /// order (Gauss–Seidel: each result is committed before the next member
1124    /// runs) until every member converges per the spec-§6 rules or
1125    /// `max_iterations` total passes (pass 1 included) have run. Hitting the
1126    /// cap keeps the last values and is NOT an error (Excel parity);
1127    /// telemetry records `capped_sccs`.
1128    Iterate {
1129        /// Total passes per SCC per recalc, pass 1 included. `1` means each
1130        /// member evaluates exactly once per recalc (the Excel accumulator
1131        /// contract, spec §7.6); `0` is a config error.
1132        max_iterations: u32,
1133        /// Absolute per-member convergence threshold on f64 serial values
1134        /// (`|Δ| < max_change`, strict — Excel semantics). Negative or
1135        /// non-finite values are config errors.
1136        max_change: f64,
1137    },
1138}
1139
1140impl CyclePolicy {
1141    /// Excel's default iterative-calculation knobs.
1142    pub const EXCEL_DEFAULT_MAX_ITERATIONS: u32 = 100;
1143    /// Excel's default maximum-change threshold.
1144    pub const EXCEL_DEFAULT_MAX_CHANGE: f64 = 0.001;
1145
1146    /// `Iterate` with Excel's defaults (100 iterations, 0.001 max change).
1147    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    /// Build full interval-tree based index during inserts (current behavior)
1158    Eager,
1159    /// Defer building any sheet index until first range query or explicit finalize
1160    Lazy,
1161    /// Use fast batch building (sorted arrays -> tree) when bulk loading, otherwise incremental
1162    FastBatch,
1163}
1164
1165pub use formualizer_common::DateSystem;
1166
1167/// Construct a new engine with the given resolver and configuration
1168pub fn new_engine<R>(resolver: R, config: EvalConfig) -> Engine<R>
1169where
1170    R: EvaluationContext + 'static,
1171{
1172    Engine::new(resolver, config)
1173}
1174
1175/// Configuration for spill behavior. Nested under EvalConfig to avoid bloating the top-level.
1176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1177pub struct SpillConfig {
1178    /// What to do when target region overlaps non-empty cells or other spills.
1179    pub conflict_policy: SpillConflictPolicy,
1180    /// Tiebreaker used when policy allows preemption or multiple anchors race.
1181    pub tiebreaker: SpillTiebreaker,
1182    /// Bounds handling when result exceeds sheet capacity.
1183    pub bounds_policy: SpillBoundsPolicy,
1184    /// Buffering approach for spill writes.
1185    pub buffer_mode: SpillBufferMode,
1186    /// Optional memory budget for shadow buffering in bytes.
1187    pub memory_budget_bytes: Option<u64>,
1188    /// Cancellation behavior while streaming rows.
1189    pub cancellation: SpillCancellationPolicy,
1190    /// Visibility policy for staged writes.
1191    pub visibility: SpillVisibility,
1192
1193    /// Hard cap on the number of cells a single spill may project.
1194    ///
1195    /// This prevents pathological vertex explosions from very large dynamic arrays.
1196    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            // Conservative: enough for common UI patterns, small enough to avoid graph blowups.
1210            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/*
1254 * Scenario: Tombstone Registry for Missing Sheets
1255 * When a sheet is deleted, formulas pointing to it become "orphans."
1256 * Instead of losing the connection, we store the formula's VertexId
1257 * under the name of the missing sheet.
1258 *
1259 * Why it matters:
1260 * This allows Sheet Addition to remain O(1) for the general case,
1261 * while providing O(N_orphans) recovery for broken formulas.
1262 */
1263#[derive(Debug, Default)]
1264pub struct TombstoneRegistry {
1265    // Maps "SheetName" -> Vec<VertexId of formulas waiting for it>
1266    pub pending_references: HashMap<String, Vec<VertexId>>,
1267}
1268
1269impl TombstoneRegistry {
1270    /// Record that a vertex is waiting for a specific sheet name to appear.
1271    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    /// Retrieve and remove all vertices waiting for a specific sheet name.
1279    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}