formualizer-eval 0.14.0

High-performance Arrow-backed Excel formula engine with dependency graph and incremental recalculation
Documentation
use crate::engine::inspect::Staleness;
use crate::engine::{CancelToken, CycleConfig, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{CellAddress, LiteralValue};
use formualizer_parse::parser::parse;
#[derive(Debug)]
struct TickingClock(std::sync::atomic::AtomicU64);
impl crate::timezone::ClockProvider for TickingClock {
    fn timezone(&self) -> &crate::timezone::TimeZoneSpec {
        &crate::timezone::TimeZoneSpec::Utc
    }
    fn now(&self) -> chrono::NaiveDateTime {
        let tick = self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        chrono::DateTime::from_timestamp(1_600_000_000 + tick as i64, 0)
            .unwrap()
            .naive_utc()
    }
}
fn engine() -> Engine<TestWorkbook> {
    let mut e = Engine::new(TestWorkbook::new(), EvalConfig::default());
    e.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(2.0))
        .unwrap();
    for (col, f) in [
        (2, "=RAND()"),
        (3, "=B1+0"),
        (4, "=NOW()"),
        (5, "=SUM(OFFSET(A1,0,0))"),
        (6, "=E1+0"),
    ] {
        e.set_cell_formula("Sheet1", 1, col, parse(f).unwrap())
            .unwrap();
    }
    e
}
#[test]
fn snapshot_is_current_and_every_followup_entry_restores_volatile_work() {
    for targeted in [0, 1, 2] {
        let mut e = engine();
        e.set_clock(std::sync::Arc::new(TickingClock(
            std::sync::atomic::AtomicU64::new(0),
        )));
        e.evaluate_all_for_snapshot(None).unwrap();
        let first = e.get_cell_value("Sheet1", 1, 2).unwrap();
        let first_now = e.get_cell_value("Sheet1", 1, 4).unwrap();
        e.set_workbook_seed(123);
        for col in 2..=6 {
            assert_eq!(
                e.inspect_cell_result(&CellAddress::new("Sheet1", 1, col).unwrap())
                    .unwrap()
                    .staleness,
                Staleness::Current
            );
        }
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 2),
            e.get_cell_value("Sheet1", 1, 3)
        );
        e.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(8.0))
            .unwrap();
        match targeted {
            0 => {
                e.evaluate_all().unwrap();
            }
            1 => {
                e.evaluate_until(&[("Sheet1", 1, 3), ("Sheet1", 1, 4), ("Sheet1", 1, 6)])
                    .unwrap();
            }
            _ => {
                e.evaluate_cell("Sheet1", 1, 3).unwrap();
                e.evaluate_cell("Sheet1", 1, 4).unwrap();
                e.evaluate_cell("Sheet1", 1, 6).unwrap();
            }
        }
        assert_ne!(e.get_cell_value("Sheet1", 1, 2), Some(first));
        assert_ne!(e.get_cell_value("Sheet1", 1, 4), Some(first_now));
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 2),
            e.get_cell_value("Sheet1", 1, 3)
        );
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 6),
            Some(LiteralValue::Number(8.0))
        );
        assert_eq!(
            e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 2).unwrap())
                .unwrap()
                .staleness,
            Staleness::Dirty
        );
    }
}
#[test]
fn cancellation_does_not_leave_snapshot_mode_active() {
    let mut e = engine();
    let token = CancelToken::new();
    token.cancel();
    assert!(e.evaluate_all_for_snapshot(Some(token)).is_err());
    e.evaluate_all().unwrap();
    assert_eq!(
        e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 2).unwrap())
            .unwrap()
            .staleness,
        Staleness::Dirty
    );
    e.evaluate_all_for_snapshot(None).unwrap();
    assert_eq!(
        e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 2).unwrap())
            .unwrap()
            .staleness,
        Staleness::Current
    );
}
#[test]
fn iterative_snapshot_defers_redirty_until_the_next_request() {
    let mut e = Engine::new(
        TestWorkbook::new(),
        EvalConfig::default().with_cycle(CycleConfig::iterate(3, 0.001)),
    );
    e.set_cell_formula("Sheet1", 1, 1, parse("=A1+1").unwrap())
        .unwrap();
    e.evaluate_all_for_snapshot(None).unwrap();
    assert_eq!(
        e.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(3.0))
    );
    assert_eq!(
        e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 1).unwrap())
            .unwrap()
            .staleness,
        Staleness::Current
    );
    e.evaluate_all().unwrap();
    assert_eq!(
        e.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(6.0))
    );
}

#[test]
fn iterative_snapshot_followup_entrypoints_restore_pending_work() {
    for route in 0..4 {
        let mut e = Engine::new(
            TestWorkbook::new(),
            EvalConfig::default().with_cycle(CycleConfig::iterate(1, 0.0)),
        );
        e.set_cell_formula("Sheet1", 1, 1, parse("=A1+1").unwrap())
            .unwrap();
        e.set_cell_formula("Sheet1", 1, 2, parse("=A1*2").unwrap())
            .unwrap();
        e.evaluate_all_for_snapshot(None).unwrap();
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 1),
            Some(LiteralValue::Number(1.0))
        );
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 2),
            Some(LiteralValue::Number(2.0))
        );
        match route {
            0 => {
                e.evaluate_until(&[("Sheet1", 1, 2)]).unwrap();
            }
            1 => {
                e.evaluate_cell("Sheet1", 1, 2).unwrap();
            }
            2 => {
                e.evaluate_all_with_delta().unwrap();
            }
            _ => {
                e.evaluate_all_for_snapshot(None).unwrap();
            }
        }
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 1),
            Some(LiteralValue::Number(2.0)),
            "route={route}"
        );
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 2),
            Some(LiteralValue::Number(4.0)),
            "route={route}"
        );
        e.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(9.0))
            .unwrap();
        e.evaluate_all_for_snapshot(None).unwrap();
        assert_eq!(
            e.get_cell_value("Sheet1", 1, 2),
            Some(LiteralValue::Number(18.0))
        );
    }
}

struct CancelAfterScc(std::sync::atomic::AtomicBool);
impl crate::function::Function for CancelAfterScc {
    fn name(&self) -> &'static str {
        "SNAPSHOT_CANCEL_AFTER_SCC"
    }
    fn eval<'a, 'b, 'c>(
        &self,
        args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
        ctx: &dyn crate::traits::FunctionContext<'b>,
    ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
        let value = args[0].value()?;
        if self.0.swap(false, std::sync::atomic::Ordering::SeqCst) {
            ctx.cancellation_token().unwrap().cancel();
            return Err(formualizer_common::ExcelError::new(
                formualizer_common::ExcelErrorKind::Cancelled,
            ));
        }
        Ok(value)
    }
}

#[test]
fn snapshot_cancel_after_scc_progress_keeps_retry_work() {
    let wb = TestWorkbook::new().with_function(std::sync::Arc::new(CancelAfterScc(
        std::sync::atomic::AtomicBool::new(true),
    )));
    let mut e = Engine::new(
        wb,
        EvalConfig::default().with_cycle(CycleConfig::iterate(1, 0.0)),
    );
    e.set_cell_formula("Sheet1", 1, 1, parse("=A1+1").unwrap())
        .unwrap();
    e.set_cell_formula(
        "Sheet1",
        1,
        2,
        parse("=SNAPSHOT_CANCEL_AFTER_SCC(A1)").unwrap(),
    )
    .unwrap();
    assert!(
        e.evaluate_all_for_snapshot(Some(CancelToken::new()))
            .is_err()
    );
    assert_eq!(
        e.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(1.0))
    );
    e.evaluate_all_for_snapshot(None).unwrap();
    assert_eq!(
        e.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(2.0))
    );
    assert_eq!(
        e.get_cell_value("Sheet1", 1, 2),
        Some(LiteralValue::Number(2.0))
    );
}

#[test]
fn resource_error_restores_the_normal_volatile_mode() {
    let mut e = engine();
    e.evaluate_all_for_snapshot(None).unwrap();
    e.set_evaluation_resource_budgets(crate::engine::EvaluationBudgets {
        work: crate::engine::WorkResourceBudget {
            max_work_units: Some(0),
        },
        ..Default::default()
    });
    assert!(e.evaluate_all_for_snapshot(None).is_err());
    e.set_evaluation_resource_budgets(Default::default());
    e.evaluate_all().unwrap();
    assert!(matches!(
        e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 2).unwrap())
            .unwrap()
            .staleness,
        Staleness::Dirty
    ));
}

struct PanicFn;
impl crate::function::Function for PanicFn {
    fn caps(&self) -> crate::function::FnCaps {
        crate::function::FnCaps::PURE
    }
    fn name(&self) -> &'static str {
        "SNAPSHOT_TEST_PANIC"
    }
    fn eval<'a, 'b, 'c>(
        &self,
        _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
        _ctx: &dyn crate::traits::FunctionContext<'b>,
    ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
        panic!("snapshot test panic")
    }
}
#[test]
fn panic_during_snapshot_evaluation_does_not_leave_snapshot_mode_active() {
    let wb = TestWorkbook::new().with_function(std::sync::Arc::new(PanicFn));
    let mut e = Engine::new(wb, EvalConfig::default());
    e.set_cell_formula("Sheet1", 1, 1, parse("=RAND()").unwrap())
        .unwrap();
    e.set_cell_formula("Sheet1", 1, 2, parse("=SNAPSHOT_TEST_PANIC()").unwrap())
        .unwrap();
    let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _ = e.evaluate_all_for_snapshot(None);
    }));
    assert!(
        caught.is_ok(),
        "the function panic must become a cell error"
    );
    assert!(!e.is_poisoned());
    assert!(
        matches!(e.get_cell_value("Sheet1", 1, 2), Some(formualizer_common::LiteralValue::Error(error)) if matches!(error.extra(), formualizer_common::ExcelErrorExtra::FunctionPanicked { .. }))
    );
    // A caller that keeps the engine and evaluates normally must get the
    // ordinary next-cycle volatile redirty again.
    e.set_cell_formula("Sheet1", 1, 2, parse("=1").unwrap())
        .unwrap();
    e.evaluate_all().unwrap();
    assert_eq!(
        e.inspect_cell_result(&CellAddress::new("Sheet1", 1, 1).unwrap())
            .unwrap()
            .staleness,
        Staleness::Dirty
    );
}