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