use std::sync::Arc;
use crate::engine::{
CycleConfig, CycleDetection, CyclePolicy, Engine, EvalConfig, FormulaIngestBatch,
FormulaIngestRecord, FormulaPlaneMode,
};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
fn authoritative_engine(detection: CycleDetection) -> Engine<TestWorkbook> {
let cfg = EvalConfig::default()
.with_formula_plane_mode(FormulaPlaneMode::AuthoritativeExperimental)
.with_cycle(CycleConfig {
detection,
policy: CyclePolicy::Error,
});
Engine::new(TestWorkbook::default(), cfg)
}
fn record(
engine: &mut Engine<TestWorkbook>,
row: u32,
col: u32,
formula: &str,
) -> FormulaIngestRecord {
let ast = parse(formula).unwrap();
let ast_id = engine.intern_formula_ast(&ast);
FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(formula)))
}
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:?}"),
}
}
fn is_circ(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> bool {
matches!(
engine.get_cell_value(sheet, row, col),
Some(LiteralValue::Error(e)) if e.kind == ExcelErrorKind::Circ
)
}
fn build_workbook(detection: CycleDetection) -> Engine<TestWorkbook> {
let mut engine = authoritative_engine(detection);
let mut col_b = Vec::new();
let mut col_e = Vec::new();
for row in 1..=120 {
engine
.set_cell_value("Sheet1", row, 1, LiteralValue::Number(row as f64))
.unwrap();
col_b.push(record(&mut engine, row, 2, &format!("=A{row}+C{row}")));
col_e.push(record(&mut engine, row, 5, &format!("=A{row}*2")));
}
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);
engine
.set_cell_formula("Sheet1", 5, 3, parse("=B5").unwrap())
.unwrap();
assert_eq!(engine.baseline_stats().formula_plane_active_span_count, 2);
engine
}
#[test]
fn span_member_in_static_cycle_is_demoted_and_circ() {
let mut engine = build_workbook(CycleDetection::Static);
let result = engine.evaluate_all().expect("eval must not bail out");
assert_eq!(result.cycle_errors, 1, "exactly one live SCC stamped");
let stats = engine.baseline_stats();
assert_eq!(
stats.formula_plane_cycle_member_span_demotions, 1,
"the column-B span must be demoted for cycle membership"
);
assert_eq!(
engine
.formula_ingest_report_total()
.fallback_reasons
.get("CycleMember")
.copied(),
Some(1),
"CycleMember fallback must be recorded in diagnostics"
);
assert!(
is_circ(&engine, "Sheet1", 5, 2),
"B5 (cycle member) is #CIRC"
);
assert!(
is_circ(&engine, "Sheet1", 5, 3),
"C5 (cycle member) is #CIRC"
);
assert_eq!(num(&engine, "Sheet1", 1, 2), 1.0, "B1 = A1 + C1 = 1");
assert_eq!(num(&engine, "Sheet1", 10, 2), 10.0, "B10 = A10 + C10 = 10");
assert_eq!(num(&engine, "Sheet1", 120, 2), 120.0, "B120 = A120 + C120");
assert_eq!(
stats.formula_plane_active_span_count, 1,
"the independent column-E span survives"
);
assert_eq!(num(&engine, "Sheet1", 1, 5), 2.0, "E1 = A1 * 2");
assert_eq!(num(&engine, "Sheet1", 120, 5), 240.0, "E120 = A120 * 2");
}
#[test]
fn span_member_in_runtime_cycle_is_demoted_and_circ() {
let mut engine = build_workbook(CycleDetection::Runtime);
let result = engine.evaluate_all().expect("eval must not bail out");
assert_eq!(result.cycle_errors, 1, "one live cycle witnessed");
let stats = engine.baseline_stats();
assert_eq!(stats.formula_plane_cycle_member_span_demotions, 1);
assert_eq!(
engine
.formula_ingest_report_total()
.fallback_reasons
.get("CycleMember")
.copied(),
Some(1)
);
assert!(is_circ(&engine, "Sheet1", 5, 2));
assert!(is_circ(&engine, "Sheet1", 5, 3));
assert_eq!(num(&engine, "Sheet1", 1, 2), 1.0);
assert_eq!(num(&engine, "Sheet1", 10, 2), 10.0);
assert_eq!(num(&engine, "Sheet1", 120, 2), 120.0);
assert_eq!(stats.formula_plane_active_span_count, 1);
assert_eq!(num(&engine, "Sheet1", 1, 5), 2.0);
assert_eq!(num(&engine, "Sheet1", 120, 5), 240.0);
}
#[test]
fn phantom_cycle_through_span_member_yields_value_under_runtime() {
let mut engine = authoritative_engine(CycleDetection::Runtime);
engine
.set_cell_value("Sheet1", 1, 6, LiteralValue::Boolean(false))
.unwrap();
let mut col_b = Vec::new();
for row in 1..=120 {
engine
.set_cell_value("Sheet1", row, 1, LiteralValue::Number(row as f64))
.unwrap();
col_b.push(record(&mut engine, row, 2, &format!("=A{row}+D{row}")));
}
engine
.ingest_formula_batches(vec![FormulaIngestBatch::new("Sheet1", col_b)])
.unwrap();
assert_eq!(engine.baseline_stats().formula_plane_active_span_count, 1);
engine
.set_cell_formula("Sheet1", 5, 4, parse("=IF(F1,B5,7)").unwrap())
.unwrap();
let result = engine.evaluate_all().expect("eval must not bail out");
assert_eq!(result.cycle_errors, 0, "phantom cycle stamps no #CIRC");
assert_eq!(
engine
.baseline_stats()
.formula_plane_cycle_member_span_demotions,
1
);
assert!(!is_circ(&engine, "Sheet1", 5, 2));
assert_eq!(
num(&engine, "Sheet1", 5, 4),
7.0,
"D5 = IF(false,...,7) = 7"
);
assert_eq!(num(&engine, "Sheet1", 5, 2), 5.0 + 7.0, "B5 = A5 + D5 = 12");
assert_eq!(
num(&engine, "Sheet1", 10, 2),
10.0,
"B10 = A10 + D10 = 10 + 0"
);
}