use std::sync::Arc;
use formualizer_common::LiteralValue;
use formualizer_parse::parser::parse;
use crate::engine::named_range::{NameScope, NamedDefinition};
use crate::engine::{
Engine, EvalConfig, FormulaIngestBatch, FormulaIngestRecord, FormulaPlaneMode,
};
use crate::reference::{CellRef, Coord, RangeRef};
use crate::test_workbook::TestWorkbook;
const SHEET: &str = "Sheet1";
const ROWS: u32 = 120;
const FIRST_ROW: u32 = 2;
const LAST_ROW: u32 = ROWS + 1;
fn engine_with_mode(mode: FormulaPlaneMode) -> Engine<TestWorkbook> {
let cfg = EvalConfig::default().with_formula_plane_mode(mode);
Engine::new(TestWorkbook::default(), cfg)
}
fn cell_ref(engine: &mut Engine<TestWorkbook>, sheet: &str, row1: u32, col1: u32) -> CellRef {
let sheet_id = engine.graph.sheet_id_mut(sheet);
CellRef::new(sheet_id, Coord::new(row1 - 1, col1 - 1, true, true))
}
fn range_def(
engine: &mut Engine<TestWorkbook>,
sheet: &str,
start_row1: u32,
start_col1: u32,
end_row1: u32,
end_col1: u32,
) -> NamedDefinition {
let start = cell_ref(engine, sheet, start_row1, start_col1);
let end = cell_ref(engine, sheet, end_row1, end_col1);
NamedDefinition::Range(RangeRef::new(start, end))
}
fn record(
engine: &mut Engine<TestWorkbook>,
row: u32,
col: u32,
formula: &str,
) -> FormulaIngestRecord {
let ast = parse(formula).unwrap_or_else(|err| panic!("parse {formula}: {err}"));
let ast_id = engine.intern_formula_ast(&ast);
FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(formula)))
}
fn ingest_column(
engine: &mut Engine<TestWorkbook>,
sheet: &str,
col: u32,
formula_for_row: impl Fn(u32) -> String,
) -> crate::engine::FormulaIngestReport {
let mut records = Vec::with_capacity(ROWS as usize);
for row in FIRST_ROW..=LAST_ROW {
let formula = formula_for_row(row);
records.push(record(engine, row, col, &formula));
}
engine
.ingest_formula_batches(vec![FormulaIngestBatch::new(sheet, records)])
.expect("ingest formulas")
}
fn literal_eq(a: &LiteralValue, b: &LiteralValue) -> bool {
fn as_num(v: &LiteralValue) -> Option<f64> {
match v {
LiteralValue::Number(n) => Some(*n),
LiteralValue::Int(i) => Some(*i as f64),
LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
_ => None,
}
}
match (as_num(a), as_num(b)) {
(Some(x), Some(y)) => {
let scale = x.abs().max(y.abs()).max(1.0);
(x - y).abs() <= scale * 1e-9
}
_ => a == b,
}
}
fn assert_column_parity(
label: &str,
sheet: &str,
col: u32,
auth: &Engine<TestWorkbook>,
off: &Engine<TestWorkbook>,
) {
for row in FIRST_ROW..=LAST_ROW {
let got = auth.get_cell_value(sheet, row, col);
let expected = off.get_cell_value(sheet, row, col);
let equal = match (&got, &expected) {
(Some(a), Some(b)) => literal_eq(a, b),
(a, b) => a == b,
};
assert!(
equal,
"{label}: value mismatch at {sheet}!R{row}C{col}: auth={got:?} off={expected:?}"
);
}
}
fn set_value(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, value: f64) {
engine
.action_atomic_journal(format!("edit {sheet}!R{row}C{col}"), |tx| {
tx.set_cell_value(sheet, row, col, LiteralValue::Number(value))?;
Ok(())
})
.unwrap();
}
fn seed_named_workbook(engine: &mut Engine<TestWorkbook>) {
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 2, LiteralValue::Number(row as f64))
.unwrap();
engine
.set_cell_value(SHEET, row, 1, LiteralValue::Number(10.0 * row as f64))
.unwrap();
}
let def = range_def(engine, SHEET, FIRST_ROW, 2, LAST_ROW, 2);
engine
.define_name("Data", def, NameScope::Workbook)
.unwrap();
}
#[test]
fn named_range_family_promotes_to_span_with_value_parity() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
seed_named_workbook(engine);
}
let report = ingest_column(&mut auth, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
assert_eq!(
report.shadow_accepted_span_cells,
u64::from(ROWS),
"named-range family must span; histogram: {:?}",
report.fallback_reasons
);
assert_eq!(report.shadow_fallback_cells, 0);
assert!(report.shadow_spans_created >= 1);
assert_eq!(auth.baseline_stats().formula_plane_active_span_count, 1);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("first eval", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
set_value(engine, SHEET, 10, 2, 5_000.0);
set_value(engine, SHEET, 11, 1, 7_000.0);
}
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("after edits", SHEET, 3, &auth, &off);
}
#[test]
fn named_range_edit_dirty_precision_is_region_bounded() {
let mut engine = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
seed_named_workbook(&mut engine);
let report = ingest_column(&mut engine, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
assert_eq!(report.shadow_accepted_span_cells, u64::from(ROWS));
engine.evaluate_all().unwrap();
let first = engine
.last_formula_plane_span_eval_report()
.expect("first eval must run the authoritative span pass");
assert_eq!(first.span_eval_placement_count, u64::from(ROWS));
set_value(&mut engine, SHEET, 10, 2, 5_000.0);
engine.evaluate_all().unwrap();
let inside = engine
.last_formula_plane_span_eval_report()
.expect("edit inside named region must produce span work");
assert_eq!(inside.span_eval_placement_count, u64::from(ROWS));
set_value(&mut engine, SHEET, 11, 1, 7_000.0);
engine.evaluate_all().unwrap();
let relative = engine
.last_formula_plane_span_eval_report()
.expect("edit of a relative precedent must produce span work");
assert_eq!(relative.span_eval_placement_count, 1);
set_value(&mut engine, SHEET, 10, 6, 9_999.0);
engine.evaluate_all().unwrap();
let outside_placements = engine
.last_formula_plane_span_eval_report()
.map(|report| report.span_eval_placement_count)
.unwrap_or(0);
assert_eq!(
outside_placements, 0,
"edit outside all read regions must not re-evaluate the span"
);
}
#[test]
fn update_name_to_new_region_invalidates_spans_and_tracks_new_region() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
seed_named_workbook(engine);
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 4, LiteralValue::Number(1_000.0 + row as f64))
.unwrap();
}
}
let report = ingest_column(&mut auth, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
assert_eq!(report.shadow_accepted_span_cells, u64::from(ROWS));
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("before update_name", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
let def = range_def(engine, SHEET, FIRST_ROW, 4, LAST_ROW, 4);
engine
.update_name("Data", def, NameScope::Workbook)
.unwrap();
}
assert_eq!(
auth.baseline_stats().formula_plane_active_span_count,
0,
"update_name must demote the name-dependent span"
);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("after update_name", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
set_value(engine, SHEET, 20, 4, 50_000.0);
}
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("edit inside re-pointed region", SHEET, 3, &auth, &off);
}
#[test]
fn sheet_scoped_define_after_ingest_invalidates_workbook_resolved_spans() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
seed_named_workbook(engine);
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 4, LiteralValue::Number(2_000.0 + row as f64))
.unwrap();
}
}
let report = ingest_column(&mut auth, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
assert_eq!(report.shadow_accepted_span_cells, u64::from(ROWS));
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("before shadowing define", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
let sheet_id = engine.graph.sheet_id_mut(SHEET);
let def = range_def(engine, SHEET, FIRST_ROW, 4, LAST_ROW, 4);
engine
.define_name("Data", def, NameScope::Sheet(sheet_id))
.unwrap();
}
assert_eq!(
auth.baseline_stats().formula_plane_active_span_count,
0,
"shadowing define must demote spans that resolved through workbook scope"
);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("after shadowing define", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
set_value(engine, SHEET, 30, 4, 77_000.0);
}
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("edit inside shadowed region", SHEET, 3, &auth, &off);
}
#[test]
fn delete_name_falls_back_to_name_error_parity() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
seed_named_workbook(engine);
}
let report = ingest_column(&mut auth, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=SUM(Data)+A{r}"));
assert_eq!(report.shadow_accepted_span_cells, u64::from(ROWS));
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
for engine in [&mut auth, &mut off] {
engine.delete_name("Data", NameScope::Workbook).unwrap();
}
assert_eq!(auth.baseline_stats().formula_plane_active_span_count, 0);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("after delete_name", SHEET, 3, &auth, &off);
match off.get_cell_value(SHEET, FIRST_ROW, 3) {
Some(LiteralValue::Error(err)) => {
assert_eq!(err.kind, formualizer_common::ExcelErrorKind::Name)
}
other => panic!("expected #NAME? ground truth after delete_name, got {other:?}"),
}
}
#[test]
fn named_cell_and_cross_sheet_named_range_span_and_track_edits() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
engine.add_sheet("Sheet2").unwrap();
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value("Sheet2", row, 2, LiteralValue::Number(row as f64))
.unwrap();
engine
.set_cell_value(SHEET, row, 1, LiteralValue::Number(3.0 * row as f64))
.unwrap();
}
engine
.set_cell_value("Sheet2", 1, 5, LiteralValue::Number(41.5))
.unwrap();
let remote = range_def(engine, "Sheet2", FIRST_ROW, 2, LAST_ROW, 2);
engine
.define_name("RemoteData", remote, NameScope::Workbook)
.unwrap();
let cell = cell_ref(engine, "Sheet2", 1, 5);
engine
.define_name(
"RemoteCell",
NamedDefinition::Cell(cell),
NameScope::Workbook,
)
.unwrap();
}
let report = ingest_column(&mut auth, SHEET, 3, |r| {
format!("=SUM(RemoteData)+RemoteCell*2+A{r}")
});
let _ = ingest_column(&mut off, SHEET, 3, |r| {
format!("=SUM(RemoteData)+RemoteCell*2+A{r}")
});
assert_eq!(
report.shadow_accepted_span_cells,
u64::from(ROWS),
"cross-sheet named range + named cell must span; histogram: {:?}",
report.fallback_reasons
);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("cross-sheet first eval", SHEET, 3, &auth, &off);
for engine in [&mut auth, &mut off] {
set_value(engine, "Sheet2", 15, 2, 12_345.0);
set_value(engine, "Sheet2", 1, 5, 100.25);
}
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("cross-sheet after edits", SHEET, 3, &auth, &off);
}
#[test]
fn shadowed_names_on_two_sheets_resolve_per_sheet_without_cross_contamination() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
engine.add_sheet("Sheet2").unwrap();
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 2, LiteralValue::Number(row as f64))
.unwrap();
engine
.set_cell_value("Sheet2", row, 2, LiteralValue::Number(1_000.0 + row as f64))
.unwrap();
engine
.set_cell_value(SHEET, row, 1, LiteralValue::Number(row as f64))
.unwrap();
engine
.set_cell_value("Sheet2", row, 1, LiteralValue::Number(row as f64))
.unwrap();
}
let workbook = range_def(engine, SHEET, FIRST_ROW, 2, LAST_ROW, 2);
engine
.define_name("X", workbook, NameScope::Workbook)
.unwrap();
let sheet2_id = engine.graph.sheet_id_mut("Sheet2");
let shadowed = range_def(engine, "Sheet2", FIRST_ROW, 2, LAST_ROW, 2);
engine
.define_name("X", shadowed, NameScope::Sheet(sheet2_id))
.unwrap();
}
for sheet in [SHEET, "Sheet2"] {
let report_auth = ingest_column(&mut auth, sheet, 3, |r| format!("=SUM(X)+A{r}"));
let _ = ingest_column(&mut off, sheet, 3, |r| format!("=SUM(X)+A{r}"));
assert_eq!(
report_auth.shadow_accepted_span_cells,
u64::from(ROWS),
"{sheet}: shadowed-name family must span; histogram: {:?}",
report_auth.fallback_reasons
);
}
assert_eq!(auth.baseline_stats().formula_plane_active_span_count, 2);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("shadowed Sheet1", SHEET, 3, &auth, &off);
assert_column_parity("shadowed Sheet2", "Sheet2", 3, &auth, &off);
let s1 = auth.get_cell_value(SHEET, FIRST_ROW, 3);
let s2 = auth.get_cell_value("Sheet2", FIRST_ROW, 3);
assert_ne!(s1, s2, "shadowed name resolutions must differ per sheet");
for engine in [&mut auth, &mut off] {
set_value(engine, "Sheet2", 40, 2, 99_000.0);
}
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("shadowed Sheet1 after Sheet2 edit", SHEET, 3, &auth, &off);
assert_column_parity("shadowed Sheet2 after edit", "Sheet2", 3, &auth, &off);
}
#[test]
fn name_covering_own_result_column_rejects_with_internal_dependency() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 2, LiteralValue::Number(row as f64))
.unwrap();
}
let def = range_def(engine, SHEET, FIRST_ROW, 3, LAST_ROW, 3);
engine
.define_name("SelfRegion", def, NameScope::Workbook)
.unwrap();
}
let report = ingest_column(&mut auth, SHEET, 3, |r| format!("=SUM(SelfRegion)*0+B{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=SUM(SelfRegion)*0+B{r}"));
assert_eq!(report.shadow_accepted_span_cells, 0);
assert_eq!(
report
.fallback_reasons
.get("InternalDependency")
.copied()
.unwrap_or(0),
u64::from(ROWS),
"histogram: {:?}",
report.fallback_reasons
);
assert_eq!(auth.baseline_stats().formula_plane_active_span_count, 0);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("self-overlap", SHEET, 3, &auth, &off);
}
#[test]
fn literal_and_undefined_names_fall_back_with_precise_reason() {
let mut auth = engine_with_mode(FormulaPlaneMode::AuthoritativeExperimental);
let mut off = engine_with_mode(FormulaPlaneMode::Off);
for engine in [&mut auth, &mut off] {
for row in FIRST_ROW..=LAST_ROW {
engine
.set_cell_value(SHEET, row, 1, LiteralValue::Number(row as f64))
.unwrap();
}
engine
.define_name(
"LitName",
NamedDefinition::Literal(LiteralValue::Number(2.5)),
NameScope::Workbook,
)
.unwrap();
}
let literal_report = ingest_column(&mut auth, SHEET, 3, |r| format!("=LitName+A{r}"));
let _ = ingest_column(&mut off, SHEET, 3, |r| format!("=LitName+A{r}"));
assert_eq!(literal_report.shadow_accepted_span_cells, 0);
assert_eq!(
literal_report
.fallback_reasons
.get("UnsupportedNamedReference")
.copied()
.unwrap_or(0),
u64::from(ROWS),
"histogram: {:?}",
literal_report.fallback_reasons
);
let undefined_report = ingest_column(&mut auth, SHEET, 4, |r| format!("=NoSuchName+A{r}"));
let _ = ingest_column(&mut off, SHEET, 4, |r| format!("=NoSuchName+A{r}"));
assert_eq!(undefined_report.shadow_accepted_span_cells, 0);
assert_eq!(
undefined_report
.fallback_reasons
.get("UnsupportedNamedReference")
.copied()
.unwrap_or(0),
u64::from(ROWS),
"histogram: {:?}",
undefined_report.fallback_reasons
);
assert_eq!(auth.baseline_stats().formula_plane_active_span_count, 0);
auth.evaluate_all().unwrap();
off.evaluate_all().unwrap();
assert_column_parity("literal-name column", SHEET, 3, &auth, &off);
assert_column_parity("undefined-name column", SHEET, 4, &auth, &off);
match off.get_cell_value(SHEET, FIRST_ROW, 3) {
Some(LiteralValue::Number(n)) => assert!((n - (2.5 + FIRST_ROW as f64)).abs() < 1e-9),
other => panic!("expected numeric literal-name ground truth, got {other:?}"),
}
match off.get_cell_value(SHEET, FIRST_ROW, 4) {
Some(LiteralValue::Error(err)) => {
assert_eq!(err.kind, formualizer_common::ExcelErrorKind::Name)
}
other => panic!("expected #NAME? ground truth for undefined name, got {other:?}"),
}
}