use crate::eval::{Context, EvalCtx, EvalOp, Registry, Span};
use crate::engine::Engine;
use crate::types::Value;
#[derive(Debug, Clone, PartialEq)]
enum Op {
Number,
Text,
Bool,
Error,
Variable(String),
Reference,
UnaryOp,
BinaryOp,
Array,
Apply,
FunctionCall(String),
}
impl Op {
fn from(op: EvalOp<'_>) -> Self {
match op {
EvalOp::Number => Op::Number,
EvalOp::Text => Op::Text,
EvalOp::Bool => Op::Bool,
EvalOp::Error(_) => Op::Error,
EvalOp::Variable(name) => Op::Variable(name.to_string()),
EvalOp::Reference => Op::Reference,
EvalOp::UnaryOp(_) => Op::UnaryOp,
EvalOp::BinaryOp(_) => Op::BinaryOp,
EvalOp::Array => Op::Array,
EvalOp::Apply => Op::Apply,
EvalOp::FunctionCall(name) => Op::FunctionCall(name.to_string()),
}
}
}
fn trace(formula: &str) -> (Value, Vec<(Op, Span, Value)>) {
let engine = Engine::sheets();
let expr = engine.parse(formula).expect("valid formula");
let registry = Registry::new();
let mut events: Vec<(Op, Span, Value)> = Vec::new();
let value = {
let mut record =
|op: EvalOp<'_>, span: Span, v: &Value| events.push((Op::from(op), span, v.clone()));
let mut ctx = EvalCtx::new(Context::empty(), ®istry);
ctx.hook = Some(&mut record);
crate::eval::evaluate_expr(&expr, &mut ctx)
};
(value, events)
}
fn eval_no_hook(formula: &str) -> Value {
let engine = Engine::sheets();
let expr = engine.parse(formula).expect("valid formula");
let registry = Registry::new();
let mut ctx = EvalCtx::new(Context::empty(), ®istry);
crate::eval::evaluate_expr(&expr, &mut ctx)
}
fn without_span(events: &[(Op, Span, Value)]) -> Vec<(Op, Value)> {
events.iter().map(|(op, _, v)| (op.clone(), v.clone())).collect()
}
fn text_of<'a>(formula: &'a str, span: Span) -> &'a str {
&formula[span.offset..span.offset + span.length]
}
fn contains(parent: Span, child: Span) -> bool {
child.offset >= parent.offset
&& child.offset + child.length <= parent.offset + parent.length
}
#[test]
fn fires_post_order_for_binary_op() {
let formula = "=1+2";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(3.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(1.0)),
(Op::Number, Value::Number(2.0)),
(Op::BinaryOp, Value::Number(3.0)),
]
);
assert_eq!(text_of(formula, events[0].1), "1");
assert_eq!(text_of(formula, events[1].1), "2");
assert_eq!(text_of(formula, events[2].1), "1+2");
assert!(contains(events[2].1, events[0].1));
assert!(contains(events[2].1, events[1].1));
}
#[test]
fn fires_once_per_node_for_function_call() {
let formula = "=SUM(1,2)";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(3.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(1.0)),
(Op::Number, Value::Number(2.0)),
(Op::FunctionCall("SUM".to_string()), Value::Number(3.0)),
]
);
assert_eq!(text_of(formula, events[0].1), "1");
assert_eq!(text_of(formula, events[1].1), "2");
assert_eq!(text_of(formula, events[2].1), "SUM(1,2)");
assert!(contains(events[2].1, events[0].1));
assert!(contains(events[2].1, events[1].1));
}
#[test]
fn nested_tree_fires_every_node_exactly_once() {
let formula = "=(1+2)*3";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(9.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(1.0)),
(Op::Number, Value::Number(2.0)),
(Op::BinaryOp, Value::Number(3.0)),
(Op::Number, Value::Number(3.0)),
(Op::BinaryOp, Value::Number(9.0)),
]
);
assert_eq!(text_of(formula, events[0].1), "1");
assert_eq!(text_of(formula, events[1].1), "2");
assert_eq!(text_of(formula, events[2].1), "1+2");
assert_eq!(text_of(formula, events[3].1), "3");
assert_eq!(text_of(formula, events[4].1), "(1+2)*3");
let root = events[4].1;
for &(_, span, _) in &events {
assert!(contains(root, span));
}
assert!(contains(events[2].1, events[0].1));
assert!(contains(events[2].1, events[1].1));
}
#[test]
fn lazy_branch_not_taken_never_fires() {
let formula = "=IF(TRUE, 10, 20)";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(10.0));
assert_eq!(
without_span(&events),
vec![
(Op::Bool, Value::Bool(true)),
(Op::Number, Value::Number(10.0)),
(Op::FunctionCall("IF".to_string()), Value::Number(10.0)),
]
);
assert!(!events.iter().any(|(_, _, v)| *v == Value::Number(20.0)));
assert_eq!(text_of(formula, events[0].1), "TRUE");
assert_eq!(text_of(formula, events[1].1), "10");
assert!(contains(events[2].1, events[0].1));
assert!(contains(events[2].1, events[1].1));
}
#[test]
fn unary_op_reports_operator_node() {
let formula = "=-5";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(-5.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(5.0)),
(Op::UnaryOp, Value::Number(-5.0)),
]
);
assert_eq!(text_of(formula, events[0].1), "5");
assert_eq!(text_of(formula, events[1].1), "-5");
}
#[test]
fn text_and_concat_report_correct_ops() {
let formula = "=\"a\"&\"b\"";
let (value, events) = trace(formula);
assert_eq!(value, Value::Text("ab".to_string()));
assert_eq!(
without_span(&events),
vec![
(Op::Text, Value::Text("a".to_string())),
(Op::Text, Value::Text("b".to_string())),
(Op::BinaryOp, Value::Text("ab".to_string())),
]
);
assert_eq!(text_of(formula, events[0].1), "\"a\"");
assert_eq!(text_of(formula, events[1].1), "\"b\"");
assert_eq!(text_of(formula, events[2].1), "\"a\"&\"b\"");
}
#[test]
fn array_literal_reports_elements_then_array() {
let formula = "={1,2}";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
);
assert_eq!(events.len(), 3);
assert_eq!(events[0].0, Op::Number);
assert_eq!(events[1].0, Op::Number);
assert_eq!(events[2].0, Op::Array);
assert_eq!(text_of(formula, events[0].1), "1");
assert_eq!(text_of(formula, events[1].1), "2");
assert_eq!(text_of(formula, events[2].1), "{1,2}");
}
#[test]
fn error_node_still_fires_with_error_value() {
let (value, events) = trace("=SQRT(-1)");
assert!(value.is_error());
let (last_op, _last_span, last_val) = events.last().expect("at least one event");
assert_eq!(*last_op, Op::FunctionCall("SQRT".to_string()));
assert!(last_val.is_error());
}
#[test]
fn none_hook_matches_some_hook_value() {
for formula in [
"=1+2",
"=(1+2)*3",
"=SUM(1,2,3)",
"=IF(FALSE, 1, 2)",
"=-5%",
"=\"x\"&\"y\"",
"={1,2,3}",
"=AVERAGE(2,4,6)",
"=LAMBDA(x, x*2)(5)",
] {
let (with_hook, _) = trace(formula);
let without_hook = eval_no_hook(formula);
assert_eq!(with_hook, without_hook, "value diverged for {formula}");
}
}
#[test]
fn apply_fires_lambda_parameter_binding_even_when_body_reads_it() {
let formula = "=LAMBDA(x, x*2)(5)";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(10.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(5.0)),
(Op::Variable("x".to_string()), Value::Number(5.0)),
(Op::Variable("x".to_string()), Value::Number(5.0)),
(Op::Number, Value::Number(2.0)),
(Op::BinaryOp, Value::Number(10.0)),
(Op::Apply, Value::Number(10.0)),
]
);
assert_eq!(text_of(formula, events[1].1), "x");
assert_ne!(events[0].1.offset, events[1].1.offset, "call arg and param token are different source positions");
assert!(!events.iter().any(|(op, _, _)| matches!(op, Op::FunctionCall(name) if name == "LAMBDA")));
let apply_span = events.last().unwrap().1;
assert_eq!(text_of(formula, apply_span), "LAMBDA(x, x*2)(5)");
for &(_, span, _) in &events {
assert!(contains(apply_span, span));
}
}
#[test]
fn apply_fires_lambda_parameter_binding_even_when_body_never_reads_it() {
let formula = "=LAMBDA(x, 42)(5)";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(42.0));
assert_eq!(
without_span(&events),
vec![
(Op::Number, Value::Number(5.0)),
(Op::Variable("x".to_string()), Value::Number(5.0)),
(Op::Number, Value::Number(42.0)),
(Op::Apply, Value::Number(42.0)),
]
);
assert_eq!(text_of(formula, events[1].1), "x");
}
#[test]
fn apply_fires_a_binding_event_per_parameter_in_order() {
let formula = "=LAMBDA(a, b, a-b)(10, 3)";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(7.0));
let bindings: Vec<(String, Value)> = events
.iter()
.filter_map(|(op, _, v)| match op {
Op::Variable(name) => Some((name.clone(), v.clone())),
_ => None,
})
.collect();
assert_eq!(bindings[0], ("a".to_string(), Value::Number(10.0)));
assert_eq!(bindings[1], ("b".to_string(), Value::Number(3.0)));
}
fn variable_events(events: &[(Op, Span, Value)]) -> Vec<(String, Value)> {
events
.iter()
.filter_map(|(op, _, v)| match op {
Op::Variable(name) => Some((name.clone(), v.clone())),
_ => None,
})
.collect()
}
#[test]
fn map_fires_one_parameter_event_per_element_even_when_body_ignores_it() {
let formula = "=MAP({1,2,3}, LAMBDA(x, 42))";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![Value::Number(42.0), Value::Number(42.0), Value::Number(42.0)])
);
let bindings = variable_events(&events);
assert_eq!(
bindings,
vec![
("x".to_string(), Value::Number(1.0)),
("x".to_string(), Value::Number(2.0)),
("x".to_string(), Value::Number(3.0)),
]
);
let param_spans: Vec<Span> = events
.iter()
.filter(|(op, _, _)| matches!(op, Op::Variable(n) if n == "x"))
.map(|(_, span, _)| *span)
.collect();
assert_eq!(param_spans.len(), 3);
assert_eq!(param_spans[0], param_spans[1]);
assert_eq!(param_spans[1], param_spans[2]);
assert_eq!(text_of(formula, param_spans[0]), "x");
}
#[test]
fn map_fires_parameter_events_when_body_reads_it_too() {
let formula = "=MAP({1,2,3}, LAMBDA(x, x*10))";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![Value::Number(10.0), Value::Number(20.0), Value::Number(30.0)])
);
let bindings = variable_events(&events);
assert_eq!(bindings.len(), 6);
for pair in bindings.chunks(2) {
assert_eq!(pair[0], pair[1]);
}
assert_eq!(bindings[0].1, Value::Number(1.0));
assert_eq!(bindings[2].1, Value::Number(2.0));
assert_eq!(bindings[4].1, Value::Number(3.0));
}
#[test]
fn reduce_fires_two_parameter_events_per_item() {
let formula = "=REDUCE(0, {1,2,3}, LAMBDA(acc,item, acc+item))";
let (value, events) = trace(formula);
assert_eq!(value, Value::Number(6.0));
let bindings = variable_events(&events);
let acc_bindings: Vec<Value> = bindings
.iter()
.filter(|(n, _)| n == "acc")
.step_by(2)
.map(|(_, v)| v.clone())
.collect();
let item_bindings: Vec<Value> = bindings
.iter()
.filter(|(n, _)| n == "item")
.step_by(2)
.map(|(_, v)| v.clone())
.collect();
assert_eq!(
acc_bindings,
vec![Value::Number(0.0), Value::Number(1.0), Value::Number(3.0)]
);
assert_eq!(
item_bindings,
vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]
);
}
#[test]
fn scan_fires_two_parameter_events_per_item() {
let formula = "=SCAN(0, {1,2,3}, LAMBDA(acc,item, acc+item))";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![Value::Number(1.0), Value::Number(3.0), Value::Number(6.0)])
);
let bindings = variable_events(&events);
let item_bindings: Vec<Value> = bindings
.iter()
.filter(|(n, _)| n == "item")
.step_by(2)
.map(|(_, v)| v.clone())
.collect();
assert_eq!(
item_bindings,
vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]
);
}
#[test]
fn byrow_fires_one_parameter_event_per_row_even_when_body_ignores_it() {
let formula = "=BYROW({1,2;3,4}, LAMBDA(row, 99))";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![
Value::Array(vec![Value::Number(99.0)]),
Value::Array(vec![Value::Number(99.0)]),
])
);
let bindings = variable_events(&events);
assert_eq!(
bindings,
vec![
(
"row".to_string(),
Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
),
(
"row".to_string(),
Value::Array(vec![Value::Number(3.0), Value::Number(4.0)])
),
]
);
}
#[test]
fn bycol_fires_one_parameter_event_per_column_even_when_body_ignores_it() {
let formula = "=BYCOL({1,2;3,4}, LAMBDA(col, 99))";
let (value, events) = trace(formula);
assert_eq!(value, Value::Array(vec![Value::Number(99.0), Value::Number(99.0)]));
let bindings = variable_events(&events);
assert_eq!(
bindings,
vec![
(
"col".to_string(),
Value::Array(vec![Value::Number(1.0), Value::Number(3.0)])
),
(
"col".to_string(),
Value::Array(vec![Value::Number(2.0), Value::Number(4.0)])
),
]
);
}
#[test]
fn makearray_fires_two_parameter_events_per_cell_even_when_body_ignores_them() {
let formula = "=MAKEARRAY(2, 2, LAMBDA(r,c, 0))";
let (value, events) = trace(formula);
assert_eq!(
value,
Value::Array(vec![
Value::Array(vec![Value::Number(0.0), Value::Number(0.0)]),
Value::Array(vec![Value::Number(0.0), Value::Number(0.0)]),
])
);
let bindings = variable_events(&events);
let r_events: Vec<Value> =
bindings.iter().filter(|(n, _)| n == "r").map(|(_, v)| v.clone()).collect();
let c_events: Vec<Value> =
bindings.iter().filter(|(n, _)| n == "c").map(|(_, v)| v.clone()).collect();
assert_eq!(
r_events,
vec![
Value::Number(1.0),
Value::Number(1.0),
Value::Number(2.0),
Value::Number(2.0)
]
);
assert_eq!(
c_events,
vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(1.0),
Value::Number(2.0)
]
);
}
#[test]
fn hof_parameter_events_do_not_change_computed_value() {
for formula in [
"=MAP({1,2,3}, LAMBDA(x, x*2))",
"=REDUCE(0, {1,2,3}, LAMBDA(acc,item, acc+item))",
"=SCAN(0, {1,2,3}, LAMBDA(acc,item, acc+item))",
"=BYROW({1,2;3,4}, LAMBDA(row, SUM(row)))",
"=BYCOL({1,2;3,4}, LAMBDA(col, SUM(col)))",
"=MAKEARRAY(2, 2, LAMBDA(r,c, r*10+c))",
] {
let (with_hook, _) = trace(formula);
let without_hook = eval_no_hook(formula);
assert_eq!(with_hook, without_hook, "value diverged for {formula}");
}
}