Skip to main content

truecalc_core/eval/
mod.rs

1pub mod context;
2pub mod coercion;
3pub mod functions;
4pub mod resolver;
5
6pub use context::Context;
7pub use functions::{EvalCtx, FunctionMeta, Registry};
8pub use resolver::{extract_refs, Resolver};
9
10use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
11use crate::types::{ErrorKind, Value};
12
13use coercion::{to_number, to_string_val};
14use functions::FunctionKind;
15
16/// Walk an expression tree and produce a [`Value`].
17///
18/// Variables are resolved from `ctx.ctx`; functions are dispatched through
19/// `ctx.registry`. Eager functions receive pre-evaluated arguments; lazy
20/// functions (e.g. `IF`) receive raw [`Expr`] nodes and control their own
21/// evaluation order.
22pub fn evaluate_expr(expr: &Expr, ctx: &mut EvalCtx<'_>) -> Value {
23    match expr {
24        // ── Leaf nodes ──────────────────────────────────────────────────────
25        Expr::Number(n, _) => {
26            if n.is_finite() {
27                Value::Number(*n)
28            } else {
29                Value::Error(ErrorKind::Num)
30            }
31        }
32        Expr::Text(s, _)   => Value::Text(s.clone()),
33        Expr::Bool(b, _)   => Value::Bool(*b),
34        // Bare identifiers: a local binding (LAMBDA parameter, caller-supplied
35        // variable, or canonical-text variable) wins; otherwise the name is
36        // classified into a `Ref` and read through the resolver (P1.3, #525).
37        Expr::Variable(name, _) => match ctx.ctx.lookup(name) {
38            Some(v) => v,
39            None => ctx.resolve_ref(&crate::parser::refs::Ref::classify(name)),
40        },
41        // Sheet-qualified references: a binding under the canonical reference
42        // text wins (back-compat with variable-supplied refs); otherwise the
43        // reference is read through the resolver.
44        Expr::Reference(r, _) => match ctx.ctx.lookup(&r.to_string()) {
45            Some(v) => v,
46            None => ctx.resolve_ref(r),
47        },
48
49        // ── Unary ops ───────────────────────────────────────────────────────
50        Expr::UnaryOp { op, operand, .. } => {
51            let val = evaluate_expr(operand, ctx);
52            match to_number(val) {
53                Err(e) => e,
54                Ok(n)  => match op {
55                    UnaryOp::Neg     => Value::Number(-n),
56                    UnaryOp::Percent => Value::Number(n / 100.0),
57                },
58            }
59        }
60
61        // ── Binary ops ──────────────────────────────────────────────────────
62        Expr::BinaryOp { op, left, right, .. } => {
63            let lv = evaluate_expr(left, ctx);
64            let rv = evaluate_expr(right, ctx);
65            eval_binary(op, lv, rv)
66        }
67
68        // ── Array literals ──────────────────────────────────────────────────
69        Expr::Array(elems, _) => {
70            let mut values = Vec::with_capacity(elems.len());
71            for elem in elems {
72                let v = evaluate_expr(elem, ctx);
73                values.push(v);
74            }
75            Value::Array(values)
76        }
77
78        // ── Immediately-invoked apply: LAMBDA(x, body)(arg) ────────────────
79        Expr::Apply { func, call_args, .. } => {
80            eval_apply(func, call_args, ctx)
81        }
82
83        // ── Function calls ──────────────────────────────────────────────────
84        Expr::FunctionCall { name, args, .. } => {
85            match ctx.registry.get(name) {
86                None => Value::Error(ErrorKind::Name),
87                Some(FunctionKind::Lazy(f)) => {
88                    // Copy the fn pointer out to avoid holding a borrow on ctx.registry
89                    // while also mutably borrowing ctx itself.
90                    let f: functions::LazyFn = *f;
91                    f(args, ctx)
92                }
93                Some(FunctionKind::Eager(f)) => {
94                    let f: functions::EagerFn = *f;
95                    // Evaluate all args; return first error encountered.
96                    let mut evaluated = Vec::with_capacity(args.len());
97                    for arg in args {
98                        let v = evaluate_expr(arg, ctx);
99                        if matches!(v, Value::Error(_)) {
100                            return v;
101                        }
102                        evaluated.push(v);
103                    }
104                    f(&evaluated)
105                }
106            }
107        }
108
109    }
110}
111
112/// Evaluate an immediately-invoked function application `func(call_args)`.
113fn eval_apply(func: &Expr, call_args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
114    let (lambda_params, body) = match func {
115        Expr::FunctionCall { name, args: lambda_args, .. } if name == "LAMBDA" => {
116            if lambda_args.is_empty() {
117                return Value::Error(ErrorKind::NA);
118            }
119            let param_count = lambda_args.len() - 1;
120            let mut params: Vec<String> = Vec::with_capacity(param_count);
121            for param_expr in &lambda_args[..param_count] {
122                match param_expr {
123                    Expr::Variable(n, _) => params.push(n.to_uppercase()),
124                    _ => return Value::Error(ErrorKind::Name),
125                }
126            }
127            let body = &lambda_args[lambda_args.len() - 1];
128            (params, body)
129        }
130        _ => return Value::Error(ErrorKind::Value),
131    };
132
133    if call_args.len() != lambda_params.len() {
134        return Value::Error(ErrorKind::NA);
135    }
136
137    let mut evaluated_args: Vec<Value> = Vec::with_capacity(call_args.len());
138    for arg in call_args {
139        let v = evaluate_expr(arg, ctx);
140        if matches!(v, Value::Error(_)) {
141            return v;
142        }
143        evaluated_args.push(v);
144    }
145
146    let mut saved: Vec<(String, Option<Value>)> = Vec::with_capacity(lambda_params.len());
147    for (param, val) in lambda_params.iter().zip(evaluated_args) {
148        let old = ctx.ctx.set(param.clone(), val);
149        saved.push((param.clone(), old));
150    }
151
152    let result = evaluate_expr(body, ctx);
153
154    for (name, old_val) in saved.into_iter().rev() {
155        match old_val {
156            Some(v) => { ctx.ctx.set(name, v); }
157            None    => { ctx.ctx.remove(&name); }
158        }
159    }
160
161    result
162}
163
164// ── Type ordering for cross-type comparisons (Excel semantics) ───────────────
165// Number < Text < Bool  (Empty counts as Number)
166fn type_rank(v: &Value) -> u8 {
167    match v {
168        Value::Number(_) | Value::Date(_) | Value::Empty | Value::Zoned(_) => 0,
169        Value::Text(_)                  => 1,
170        Value::Bool(_)                  => 2,
171        // Error and Array cannot reach compare_values through the normal eval path
172        // (eval_binary guards against errors before calling compare_values).
173        Value::Error(_) | Value::Array(_) => 3,
174    }
175}
176
177fn eval_binary(op: &BinaryOp, lv: Value, rv: Value) -> Value {
178    // ── Array broadcasting ───────────────────────────────────────────────────
179    match (&lv, &rv) {
180        (Value::Array(lelems), Value::Array(relems)) => {
181            // Element-wise operation when both operands are arrays of the same length.
182            if lelems.len() != relems.len() {
183                return Value::Error(ErrorKind::Value);
184            }
185            let result: Vec<Value> = lelems
186                .iter()
187                .zip(relems.iter())
188                .map(|(l, r)| eval_binary(op, l.clone(), r.clone()))
189                .collect();
190            return Value::Array(result);
191        }
192        (Value::Array(elems), _) => {
193            let result: Vec<Value> = elems
194                .iter()
195                .map(|e| eval_binary(op, e.clone(), rv.clone()))
196                .collect();
197            return Value::Array(result);
198        }
199        (_, Value::Array(elems)) => {
200            let result: Vec<Value> = elems
201                .iter()
202                .map(|e| eval_binary(op, lv.clone(), e.clone()))
203                .collect();
204            return Value::Array(result);
205        }
206        _ => {}
207    }
208    match op {
209        // ── Arithmetic ──────────────────────────────────────────────────────
210        BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Pow => {
211            // Date-type production (schema spec §6: date-typed iff the fixtures
212            // pipeline observes Sheets producing a Date). Observed: date ± offset
213            // via `+` stays date-typed (workbook.tsv `=DATE(2026,6,7)+1` → date);
214            // date − date is a plain number (`=DATE(2026,6,7)-DATE(2026,6,1)` → 6,
215            // `=TODAY()-TODAY()` → 0, both typed number). Everything else —
216            // including date − number, ×, ÷, ^ — stays a plain number until a
217            // fixture observes otherwise.
218            let date_typed_add = matches!(op, BinaryOp::Add)
219                && matches!(lv, Value::Date(_)) != matches!(rv, Value::Date(_));
220            let ln = match to_number(lv) { Ok(n) => n, Err(e) => return e };
221            let rn = match to_number(rv) { Ok(n) => n, Err(e) => return e };
222            let result = match op {
223                BinaryOp::Add => ln + rn,
224                BinaryOp::Sub => ln - rn,
225                BinaryOp::Mul => ln * rn,
226                BinaryOp::Div => {
227                    if rn == 0.0 {
228                        return Value::Error(ErrorKind::DivByZero);
229                    }
230                    ln / rn
231                }
232                BinaryOp::Pow => libm::pow(ln, rn),
233                // Safety: outer match arm covers exactly Add|Sub|Mul|Div|Pow; Concat and comparison ops are handled separately.
234                _ => unreachable!(),
235            };
236            if !result.is_finite() {
237                return Value::Error(ErrorKind::Num);
238            }
239            if date_typed_add {
240                Value::Date(result)
241            } else {
242                Value::Number(result)
243            }
244        }
245
246        // ── Concatenation ───────────────────────────────────────────────────
247        BinaryOp::Concat => {
248            let ls = match to_string_val(lv) { Ok(s) => s, Err(e) => return e };
249            let rs = match to_string_val(rv) { Ok(s) => s, Err(e) => return e };
250            Value::Text(ls + &rs)
251        }
252
253        // ── Comparisons ─────────────────────────────────────────────────────
254        BinaryOp::Eq | BinaryOp::Ne
255        | BinaryOp::Lt | BinaryOp::Gt
256        | BinaryOp::Le | BinaryOp::Ge => {
257            // Error propagation: left side first.
258            if let Value::Error(_) = &lv { return lv; }
259            if let Value::Error(_) = &rv { return rv; }
260            // Mixed naive/aware comparison is rejected (a zoned instant cannot be
261            // ordered against a naive value).
262            if matches!(&lv, Value::Zoned(_)) ^ matches!(&rv, Value::Zoned(_)) {
263                return Value::Error(ErrorKind::Value);
264            }
265
266            let result = compare_values(op, &lv, &rv);
267            Value::Bool(result)
268        }
269    }
270}
271
272/// Compare two (non-error) values with Excel ordering semantics.
273fn compare_values(op: &BinaryOp, lv: &Value, rv: &Value) -> bool {
274    match (lv, rv) {
275        (Value::Number(a), Value::Number(b)) => apply_cmp(op, a.partial_cmp(b)),
276        (Value::Date(a),   Value::Date(b))   => apply_cmp(op, a.partial_cmp(b)),
277        (Value::Date(a),   Value::Number(b)) => apply_cmp(op, a.partial_cmp(b)),
278        (Value::Number(a), Value::Date(b))   => apply_cmp(op, a.partial_cmp(b)),
279        // Zoned instants compare on the absolute instant only (same moment in a
280        // different zone compares equal). Cross-type Zoned is rejected in eval_binary.
281        (Value::Zoned(a),  Value::Zoned(b))  => apply_cmp(op, Some(a.utc_nanos.cmp(&b.utc_nanos))),
282        (Value::Text(a),   Value::Text(b))   => apply_cmp(op, Some(a.cmp(b))),
283        (Value::Bool(a),   Value::Bool(b))   => apply_cmp(op, Some(a.cmp(b))),
284        (Value::Empty,     Value::Empty)     => apply_cmp(op, Some(std::cmp::Ordering::Equal)),
285        // Empty acts as Number(0)
286        (Value::Empty, Value::Number(b))     => apply_cmp(op, 0.0f64.partial_cmp(b)),
287        (Value::Number(a), Value::Empty)     => apply_cmp(op, a.partial_cmp(&0.0f64)),
288        // Cross-type: use type rank
289        _ => {
290            let lr = type_rank(lv);
291            let rr = type_rank(rv);
292            match op {
293                BinaryOp::Eq => false,
294                BinaryOp::Ne => true,
295                BinaryOp::Lt => lr < rr,
296                BinaryOp::Gt => lr > rr,
297                BinaryOp::Le => lr <= rr,
298                BinaryOp::Ge => lr >= rr,
299                // Safety: outer match arm covers exactly Eq|Ne|Lt|Gt|Le|Ge; arithmetic and Concat ops are handled separately.
300                _ => unreachable!(),
301            }
302        }
303    }
304}
305
306fn apply_cmp(op: &BinaryOp, ord: Option<std::cmp::Ordering>) -> bool {
307    match ord {
308        // NaN: per Value::Number invariant this should not occur after the is_finite() guard;
309        // returning false matches Excel semantics if it somehow does.
310        None => false,
311        Some(o) => match op {
312            BinaryOp::Eq => o.is_eq(),
313            BinaryOp::Ne => o.is_ne(),
314            BinaryOp::Lt => o.is_lt(),
315            BinaryOp::Gt => o.is_gt(),
316            BinaryOp::Le => o.is_le(),
317            BinaryOp::Ge => o.is_ge(),
318            // Safety: apply_cmp is only called from compare_values which is only called from eval_binary's comparison arm (Eq|Ne|Lt|Gt|Le|Ge).
319            _ => unreachable!(),
320        },
321    }
322}
323
324// ── Tests ────────────────────────────────────────────────────────────────────
325#[cfg(test)]
326mod tests;