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