use crate::engine::{
CycleConfig, Engine, EvalConfig, FormulaIngestBatch, FormulaIngestRecord, FormulaPlaneMode,
};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
fn iterate_cfg(max_iterations: u32, max_change: f64) -> EvalConfig {
EvalConfig::default().with_cycle(CycleConfig::iterate(max_iterations, max_change))
}
fn iterate_engine(max_iterations: u32, max_change: f64) -> Engine<TestWorkbook> {
Engine::new(TestWorkbook::new(), iterate_cfg(max_iterations, max_change))
}
fn set_formula(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, f: &str) {
engine
.set_cell_formula(sheet, row, col, parse(f).expect("parse"))
.expect("set formula");
}
fn set_value(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, v: LiteralValue) {
engine
.set_cell_value(sheet, row, col, v)
.expect("set value");
}
fn num(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> f64 {
match engine.get_cell_value(sheet, row, col) {
Some(LiteralValue::Number(n)) => n,
Some(LiteralValue::Int(i)) => i as f64,
other => panic!("expected number at {sheet} r{row}c{col}, got {other:?}"),
}
}
#[test]
fn iterating_scc_inside_fp_authoritative_mode_end_to_end() {
let cfg = EvalConfig::default()
.with_formula_plane_mode(FormulaPlaneMode::AuthoritativeExperimental)
.with_cycle(CycleConfig::iterate(4, 0.001));
let mut engine = Engine::new(TestWorkbook::default(), cfg);
let mut col_b = Vec::new();
let mut col_e = Vec::new();
for row in 1..=120u32 {
set_value(
&mut engine,
"Sheet1",
row,
1,
LiteralValue::Number(row as f64),
);
for (col, f, bucket) in [
(2u32, format!("=A{row}+C{row}"), &mut col_b),
(5u32, format!("=A{row}*2"), &mut col_e),
] {
let ast = parse(&f).unwrap();
let ast_id = engine.intern_formula_ast(&ast);
bucket.push(FormulaIngestRecord::new(
row,
col,
ast_id,
Some(Arc::<str>::from(f.as_str())),
));
}
}
engine
.ingest_formula_batches(vec![FormulaIngestBatch::new(
"Sheet1",
col_b.into_iter().chain(col_e).collect(),
)])
.unwrap();
assert_eq!(engine.baseline_stats().formula_plane_active_span_count, 2);
set_formula(&mut engine, "Sheet1", 5, 3, "=B5");
engine.evaluate_all().unwrap();
let stats = engine.baseline_stats();
assert_eq!(stats.formula_plane_cycle_member_span_demotions, 1);
assert_eq!(stats.formula_plane_active_span_count, 1);
assert_eq!(num(&engine, "Sheet1", 5, 2), 20.0);
assert_eq!(num(&engine, "Sheet1", 5, 3), 20.0);
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(t.capped_sccs, 1);
assert_eq!(t.circ_cells_stamped, 0, "Iterate must not stamp #CIRC");
assert_eq!(num(&engine, "Sheet1", 1, 2), 1.0);
assert_eq!(num(&engine, "Sheet1", 120, 2), 120.0);
assert_eq!(num(&engine, "Sheet1", 120, 5), 240.0);
engine.evaluate_all().unwrap();
assert_eq!(num(&engine, "Sheet1", 5, 2), 40.0);
assert_eq!(num(&engine, "Sheet1", 120, 5), 240.0, "span stays exact");
}
#[test]
fn cancellation_mid_iteration_leaves_committed_values_and_engine_recovers() {
use crate::args::ArgSchema;
use crate::function::{FnCaps, Function};
use crate::traits::{ArgumentHandle, FunctionContext};
#[derive(Debug)]
struct CancelAt {
calls: Arc<AtomicUsize>,
trip_at: usize,
flag: Arc<AtomicBool>,
}
impl Function for CancelAt {
fn caps(&self) -> FnCaps {
FnCaps::empty()
}
fn name(&self) -> &'static str {
"CANCELAT"
}
fn arg_schema(&self) -> &'static [ArgSchema] {
&[]
}
fn eval<'a, 'b, 'c>(
&self,
_args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
let n = self.calls.fetch_add(1, Ordering::Relaxed) + 1;
if n == self.trip_at {
self.flag.store(true, Ordering::Relaxed);
}
Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(0)))
}
}
let calls = Arc::new(AtomicUsize::new(0));
let flag = Arc::new(AtomicBool::new(false));
let wb = TestWorkbook::new().with_function(Arc::new(CancelAt {
calls: calls.clone(),
trip_at: 3, flag: flag.clone(),
}));
let mut engine = Engine::new(wb, iterate_cfg(100, 0.001));
set_formula(&mut engine, "Sheet1", 1, 1, "=A2+1+CANCELAT()");
set_formula(&mut engine, "Sheet1", 2, 1, "=A1+1");
let err = engine.evaluate_all_cancellable(flag.clone()).unwrap_err();
assert_eq!(err.kind, ExcelErrorKind::Cancelled);
assert_eq!(num(&engine, "Sheet1", 1, 1), 5.0);
assert_eq!(num(&engine, "Sheet1", 2, 1), 6.0);
assert_eq!(calls.load(Ordering::Relaxed), 3, "exactly 3 passes ran");
flag.store(false, Ordering::Relaxed);
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(t.capped_sccs, 1);
assert_eq!(num(&engine, "Sheet1", 1, 1), 205.0);
assert_eq!(num(&engine, "Sheet1", 2, 1), 206.0);
}
#[test]
fn delta_sets_stay_exact_across_converging_then_stable_then_perturbed_recalcs() {
use formualizer_common::PackedSheetCell;
let mut engine = iterate_engine(100, 0.001);
set_value(&mut engine, "Sheet1", 1, 4, LiteralValue::Number(10.0)); set_formula(&mut engine, "Sheet1", 1, 1, "=IF(B1>D1,D1,B1+1)"); set_formula(&mut engine, "Sheet1", 1, 2, "=A1"); let sheet_id = engine.sheet_id("Sheet1").unwrap();
let a1 = PackedSheetCell::try_new(sheet_id, 0, 0).unwrap();
let b1 = PackedSheetCell::try_new(sheet_id, 0, 1).unwrap();
let d1 = PackedSheetCell::try_new(sheet_id, 0, 3).unwrap();
let (_res, delta) = engine.evaluate_all_with_delta().unwrap();
let mut expected = vec![a1, b1];
expected.sort_unstable();
assert_eq!(delta.changed_cells, expected, "recalc 1: both members");
let (_res, delta) = engine.evaluate_all_with_delta().unwrap();
let (_res2, delta2) = engine.evaluate_all_with_delta().unwrap();
assert!(delta.changed_cells.is_empty() || delta.changed_cells == expected);
assert!(
delta2.changed_cells.is_empty(),
"stable SCC must not delta: {:?}",
delta2.changed_cells
);
let _ = d1;
set_value(&mut engine, "Sheet1", 1, 4, LiteralValue::Number(3.0));
let (_res, delta) = engine.evaluate_all_with_delta().unwrap();
assert_eq!(delta.changed_cells, expected, "perturbed: members re-delta");
}
#[test]
fn rand_in_cycle_is_pass_stable_per_recalc_and_reseeds_across_recalcs() {
crate::builtins::random::register_builtins();
fn run(seed: u64) -> Vec<f64> {
let cfg = EvalConfig {
workbook_seed: seed,
..iterate_cfg(100, 0.001)
};
let mut engine = Engine::new(TestWorkbook::new(), cfg);
engine.set_volatile_level(crate::traits::VolatileLevel::OnRecalc);
set_formula(&mut engine, "Sheet1", 1, 2, "=RAND()+0*C1");
set_formula(&mut engine, "Sheet1", 1, 3, "=B1");
let mut out = Vec::new();
for _ in 0..3 {
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(
t.converged_sccs, 1,
"pass-stable volatile must converge immediately"
);
assert_eq!(t.settle_passes_total, 2);
assert_eq!(t.max_abs_delta_at_stop, 0.0);
assert_eq!(
num(&engine, "Sheet1", 1, 2),
num(&engine, "Sheet1", 1, 3),
"both members carry the same sample"
);
out.push(num(&engine, "Sheet1", 1, 2));
}
out
}
let a = run(42);
let b = run(42);
assert_eq!(a, b, "same seed ⇒ identical sequence across recalcs");
assert!(
a[0] != a[1] || a[1] != a[2],
"epoch reseeding must move the sample across recalcs: {a:?}"
);
}
#[test]
fn indirect_target_computed_by_the_cycle_itself_terminates_and_settles() {
let cfg = iterate_cfg(50, 0.001).with_virtual_dep_telemetry(true);
let mut engine = Engine::new(TestWorkbook::new(), cfg);
set_value(&mut engine, "Sheet1", 1, 24, LiteralValue::Number(100.0)); set_formula(&mut engine, "Sheet1", 1, 1, "=IFERROR(INDIRECT(T1),0)+1"); set_formula(&mut engine, "Sheet1", 1, 20, "=IF(A1>5,\"X1\",\"A1\")"); engine.evaluate_all().unwrap();
const MAX_REPLAN: usize = 5;
let vt = engine.last_virtual_dep_telemetry().clone();
assert!(vt.replan_iterations <= MAX_REPLAN);
assert!(
engine.last_cycle_telemetry().settle_passes_total <= (1 + MAX_REPLAN) * 50,
"bounded by replan × iteration budget"
);
engine.evaluate_all().unwrap();
assert_eq!(num(&engine, "Sheet1", 1, 1), 101.0, "A1 = X1 + 1");
assert_eq!(
engine.get_cell_value("Sheet1", 1, 20),
Some(LiteralValue::Text("X1".to_string()))
);
engine.evaluate_all().unwrap();
assert_eq!(num(&engine, "Sheet1", 1, 1), 101.0);
let t = engine.last_cycle_telemetry();
assert_eq!(t.converged_sccs, t.iterated_sccs, "no caps at fixed point");
}