Skip to main content

truecalc_core/eval/functions/
mod.rs

1pub mod array;
2pub mod database;
3pub mod date;
4pub mod engineering;
5pub mod filter;
6pub mod financial;
7pub mod google;
8pub mod logical;
9pub mod lookup;
10pub mod math;
11pub mod operator;
12pub mod parser;
13pub mod query;
14pub mod statistical;
15pub mod text;
16pub mod timezone;
17pub mod web;
18
19use std::collections::HashMap;
20use crate::eval::context::Context;
21use crate::eval::resolver::Resolver;
22use crate::parser::ast::{BinaryOp, Expr, Span, UnaryOp};
23use crate::types::{ErrorKind, Value};
24
25// ── EvalOp / EvalHook (per-node observation seam, issue #732; span-carrying
26// enhancement per distributions ADR D10) ────────────────────────────────────
27
28/// A lightweight, borrowed description of the operation an evaluated node
29/// performs. Handed to an [`EvalHook`] alongside the node's resulting
30/// [`Value`] so an observer can profile or trace evaluation without touching
31/// the AST directly or any function. Constructing it borrows from the
32/// expression and allocates nothing.
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub enum EvalOp<'a> {
35    /// Numeric literal.
36    Number,
37    /// Text literal.
38    Text,
39    /// Boolean literal.
40    Bool,
41    /// Error literal (`#REF!` etc.), carrying its kind.
42    Error(&'a ErrorKind),
43    /// Bare-identifier read (local binding or reference), carrying its name.
44    Variable(&'a str),
45    /// Cell / range / name reference read.
46    Reference,
47    /// Unary operator (negation, percent).
48    UnaryOp(&'a UnaryOp),
49    /// Binary operator (arithmetic, comparison, concatenation).
50    BinaryOp(&'a BinaryOp),
51    /// Array literal.
52    Array,
53    /// Immediately-invoked lambda application.
54    Apply,
55    /// Function call, carrying the (uppercased) function name.
56    FunctionCall(&'a str),
57}
58
59impl<'a> EvalOp<'a> {
60    /// Derive the operation descriptor for an expression node. Pure and
61    /// allocation-free — every variant only borrows from `expr`.
62    pub fn of(expr: &'a Expr) -> Self {
63        match expr {
64            Expr::Number(..) => EvalOp::Number,
65            Expr::Text(..) => EvalOp::Text,
66            Expr::Bool(..) => EvalOp::Bool,
67            Expr::Error(kind, _) => EvalOp::Error(kind),
68            Expr::Variable(name, _) => EvalOp::Variable(name),
69            Expr::Reference(..) => EvalOp::Reference,
70            Expr::UnaryOp { op, .. } => EvalOp::UnaryOp(op),
71            Expr::BinaryOp { op, .. } => EvalOp::BinaryOp(op),
72            Expr::Array(..) => EvalOp::Array,
73            Expr::Apply { .. } => EvalOp::Apply,
74            Expr::FunctionCall { name, .. } => EvalOp::FunctionCall(name),
75        }
76    }
77}
78
79/// An observer invoked once per evaluated node, in post-order (children before
80/// parents), with the node's [`EvalOp`], its source [`Span`], and resulting
81/// [`Value`]. Purely observational: it is handed shared/by-value data and
82/// cannot alter evaluation.
83///
84/// # Why `Span`
85///
86/// A single post-order stream is ambiguous for variable/dynamic-arity nodes
87/// (`FunctionCall`, `Array`, `Apply`; lazy `IF`/`AND`/`OR` that skip un-taken
88/// branches): a consumer cannot tell, from operation + value alone, which
89/// events belong to which parent, or how many children a node had. Carrying
90/// each node's byte-range `Span` — every `Expr` already has one — lets a
91/// consumer reconstruct the full tree from the flat stream by *span
92/// containment* (a child's span always falls inside its parent's), which is
93/// robust to short-circuiting by construction: an unfired branch simply has
94/// no event, and containment among the events that *do* fire is unaffected.
95/// The span doubles as the byte range a UI highlights to explain a node (see
96/// distributions ADR D10).
97///
98/// # Apply / LAMBDA callee (see [`EvalOp::Variable`] parameter-binding note)
99///
100/// The `LAMBDA(...)` callee of an `Apply` is pattern-destructured, not
101/// evaluated, so it never reduces to a `Value` and its own `FunctionCall`
102/// node never fires — there is no honest `Value` to give a lambda (no such
103/// [`Value`] variant exists). Each parameter *binding* does have an honest
104/// value, though (the argument bound to it), so it fires as an ordinary
105/// [`EvalOp::Variable`] event at bind time, carrying the parameter's own
106/// span and bound value — see `eval_apply`. A consumer can still recover the
107/// callee's source extent (it is a sub-span of the `Apply` node's span) but
108/// gets no discrete event, and no value, for the callee as a whole.
109///
110/// The same parameter-binding event fires for lambda parameters bound by the
111/// six higher-order array functions — MAP, REDUCE, BYROW, BYCOL, SCAN,
112/// MAKEARRAY — which route every lambda call through their own
113/// `crate::eval::functions::array::apply_lambda` helper rather than
114/// `eval_apply` (this was a gap in the initial parameter-event landing,
115/// closed as a follow-up). Each invocation of the lambda (one per array
116/// element for MAP, one per row for BYROW, one per accumulator step for
117/// REDUCE/SCAN, one per (row, col) cell for MAKEARRAY) fires one
118/// [`EvalOp::Variable`] event per parameter, all sharing that parameter's
119/// source span but each carrying the value bound for that specific
120/// invocation — so e.g. `MAP({1,2,3}, LAMBDA(x, 42))` fires three `x` events
121/// (values 1, 2, 3) even though the body never reads `x`. These are
122/// intentionally not deduplicated by span: a consumer sees one event per
123/// invocation, the same way a cell reference read inside a loop fires once
124/// per read rather than once per distinct span.
125///
126/// Blanket-implemented for every `FnMut(EvalOp<'_>, Span, &Value)`, so a
127/// closure can be wired directly. Wiring is opt-in via [`EvalCtx::hook`]:
128/// when it is `None` no descriptor is built and the only per-node cost is a
129/// single branch; when present, each node costs one `EvalOp::of`, one
130/// `Span` copy (two `usize`s), and one dynamic (vtable) call through the
131/// `&mut dyn EvalHook` trait object.
132pub trait EvalHook {
133    fn on_node(&mut self, op: EvalOp<'_>, span: Span, value: &Value);
134}
135
136impl<F: FnMut(EvalOp<'_>, Span, &Value)> EvalHook for F {
137    fn on_node(&mut self, op: EvalOp<'_>, span: Span, value: &Value) {
138        self(op, span, value)
139    }
140}
141
142// ── EvalCtx ───────────────────────────────────────────────────────────────
143
144/// Bundles the variable context, function registry, and reference resolver
145/// for use during evaluation. Passed to lazy functions so they can recursively
146/// evaluate sub-expressions.
147///
148/// References that are not bound as local variables (e.g. a LAMBDA parameter)
149/// are read through `resolver`; see [`crate::Resolver`]. When `resolver` is
150/// `None` (the default, via [`EvalCtx::new`]) every such reference reads as
151/// [`Value::Empty`], preserving the historical contract of
152/// [`crate::Engine::evaluate`].
153pub struct EvalCtx<'r> {
154    pub ctx: Context,
155    pub registry: &'r Registry,
156    /// Resolver for references not bound as local variables. `None` ⇒ such
157    /// references read as [`Value::Empty`] (the historical
158    /// [`crate::Engine::evaluate`] contract).
159    pub resolver: Option<&'r mut dyn Resolver>,
160    /// Opt-in per-node observation hook (issue #732). `None` (the default) ⇒
161    /// zero per-node work beyond a single branch; the callback only observes
162    /// and can never alter evaluation. Set the field directly to wire one.
163    pub hook: Option<&'r mut dyn EvalHook>,
164}
165
166impl<'r> EvalCtx<'r> {
167    /// Build an `EvalCtx` with no resolver: unbound references read as
168    /// [`Value::Empty`]. Use [`EvalCtx::with_resolver`] to supply real
169    /// workbook semantics.
170    pub fn new(ctx: Context, registry: &'r Registry) -> Self {
171        Self { ctx, registry, resolver: None, hook: None }
172    }
173
174    /// Build an `EvalCtx` that resolves references through `resolver`.
175    pub fn with_resolver(
176        ctx: Context,
177        registry: &'r Registry,
178        resolver: &'r mut dyn Resolver,
179    ) -> Self {
180        Self { ctx, registry, resolver: Some(resolver), hook: None }
181    }
182
183    /// Resolve a reference that was not bound as a local variable, delegating
184    /// to [`EvalCtx::resolver`] when present and falling back to
185    /// [`Value::Empty`] otherwise.
186    pub fn resolve_ref(&mut self, r: &crate::parser::refs::Ref) -> Value {
187        match self.resolver {
188            Some(ref mut res) => res.resolve(r),
189            None => Value::Empty,
190        }
191    }
192}
193
194// ── Function kinds ─────────────────────────────────────────────────────────
195
196/// A function that receives pre-evaluated arguments.
197/// Argument errors are caught before dispatch — the slice never contains `Value::Error`.
198pub type EagerFn = fn(&[Value]) -> Value;
199
200/// A function that receives raw AST nodes and controls its own evaluation order.
201/// Used for short-circuit operators like `IF`, `AND`, `OR`.
202pub type LazyFn  = fn(&[Expr], &mut EvalCtx<'_>) -> Value;
203
204#[derive(Clone)]
205pub enum FunctionKind {
206    Eager(EagerFn),
207    Lazy(LazyFn),
208}
209
210// ── FunctionMeta ──────────────────────────────────────────────────────────
211
212/// Metadata for a user-facing spreadsheet function.
213/// Co-located with the registration call so it can never drift.
214#[derive(Debug, Clone)]
215pub struct FunctionMeta {
216    pub category: &'static str,
217    pub signature: &'static str,
218    pub description: &'static str,
219}
220
221/// A metadata entry returned by `Registry::get_metadata()`.
222pub struct FunctionMetaEntry<'a> {
223    pub name: &'a str,
224    pub meta: &'a FunctionMeta,
225}
226
227// ── Registry ──────────────────────────────────────────────────────────────
228
229/// The runtime registry of built-in and user-registered spreadsheet functions.
230pub struct Registry {
231    pub functions: HashMap<String, FunctionKind>,
232    pub metadata: HashMap<String, FunctionMeta>,
233}
234
235impl Registry {
236    pub fn new() -> Self {
237        let mut r = Self { functions: HashMap::new(), metadata: HashMap::new() };
238        math::register_math(&mut r);
239        logical::register_logical(&mut r);
240        text::register_text(&mut r);
241        financial::register_financial(&mut r);
242        statistical::register_statistical(&mut r);
243        operator::register_operator(&mut r);
244        date::register_date(&mut r);
245        parser::register_parser(&mut r);
246        engineering::register_engineering(&mut r);
247        filter::register_filter(&mut r);
248        array::register_array(&mut r);
249        database::register_database(&mut r);
250        lookup::register_lookup(&mut r);
251        query::register_query(&mut r);
252        google::register_google(&mut r);
253        web::register_web(&mut r);
254        timezone::register_timezone(&mut r);
255        r
256    }
257
258    /// Register a user-facing eager function with metadata.
259    /// Appears in `list_functions()`.
260    /// Panics if `name` is already registered (duplicate registration).
261    pub fn register_eager(&mut self, name: &str, f: EagerFn, meta: FunctionMeta) {
262        let key = name.to_uppercase();
263        assert!(
264            !self.functions.contains_key(&key),
265            "duplicate function registration: '{}'",
266            key
267        );
268        self.functions.insert(key.clone(), FunctionKind::Eager(f));
269        self.metadata.insert(key, meta);
270    }
271
272    /// Register a user-facing lazy function with metadata.
273    /// Appears in `list_functions()`.
274    /// Panics if `name` is already registered (duplicate registration).
275    pub fn register_lazy(&mut self, name: &str, f: LazyFn, meta: FunctionMeta) {
276        let key = name.to_uppercase();
277        assert!(
278            !self.functions.contains_key(&key),
279            "duplicate function registration: '{}'",
280            key
281        );
282        self.functions.insert(key.clone(), FunctionKind::Lazy(f));
283        self.metadata.insert(key, meta);
284    }
285
286    /// Register `alias` as an alternate name for `canonical`.
287    /// The alias shares the same handler but does NOT appear in function metadata
288    /// (it will not show up in `list_functions()` or autocomplete).
289    /// Panics if `alias` is already registered or `canonical` is not yet registered.
290    pub fn register_alias(&mut self, alias: &str, canonical: &str) {
291        let alias_key = alias.to_uppercase();
292        let canonical_key = canonical.to_uppercase();
293        assert!(
294            !self.functions.contains_key(&alias_key),
295            "duplicate function registration: '{}'",
296            alias_key
297        );
298        let kind = self
299            .functions
300            .get(&canonical_key)
301            .unwrap_or_else(|| {
302                panic!(
303                    "register_alias: canonical '{}' must be registered before alias '{}'",
304                    canonical_key, alias_key
305                )
306            })
307            .clone();
308        self.functions.insert(alias_key, kind);
309        // Intentionally no metadata entry — aliases are not user-facing
310    }
311
312    /// Register an internal/compiler-only eager function without metadata.
313    /// Never appears in `list_functions()`.
314    pub fn register_internal(&mut self, name: &str, f: EagerFn) {
315        self.functions.insert(name.to_uppercase(), FunctionKind::Eager(f));
316    }
317
318    /// Register an internal/compiler-only lazy function without metadata.
319    /// Never appears in `list_functions()`.
320    pub fn register_internal_lazy(&mut self, name: &str, f: LazyFn) {
321        self.functions.insert(name.to_uppercase(), FunctionKind::Lazy(f));
322    }
323
324    pub fn get(&self, name: &str) -> Option<&FunctionKind> {
325        self.functions.get(&name.to_uppercase())
326    }
327
328    /// Iterate all user-facing functions with their metadata.
329    /// The registry is the single source of truth — this can never drift.
330    pub fn list_functions(&self) -> impl Iterator<Item = (&str, &FunctionMeta)> {
331        self.metadata.iter().map(|(k, v)| (k.as_str(), v))
332    }
333
334    /// Return all function metadata entries as a Vec of named structs.
335    /// Used for inspection (e.g. counting functions, verifying aliases are absent).
336    pub fn get_metadata(&self) -> Vec<FunctionMetaEntry<'_>> {
337        self.metadata
338            .iter()
339            .map(|(k, v)| FunctionMetaEntry { name: k.as_str(), meta: v })
340            .collect()
341    }
342
343    /// Return all user-facing function names (from metadata, not aliases).
344    pub fn metadata_names(&self) -> Vec<String> {
345        self.metadata.keys().cloned().collect()
346    }
347}
348
349impl Registry {
350    /// Volatile functions — outputs change on every evaluation.
351    /// Excluded from conformance fixtures; covered by property tests instead.
352    pub const VOLATILE_FUNCTIONS: &'static [&'static str] = &[
353        "RAND", "RANDARRAY", "NOW", "TODAY", "RANDBETWEEN", "TZNOW",
354    ];
355}
356
357impl Default for Registry {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363/// Placeholder that stands in for the function name inside an arity diagnostic
364/// message. `check_arity`/`check_arity_len` do not know the calling function's
365/// name, so they emit this token; the evaluator substitutes the real name at
366/// the dispatch site (see [`crate::eval::finalize_call_result`]). Chosen from
367/// control characters so it can never collide with a real function name.
368pub const FN_NAME_PLACEHOLDER: &str = "\u{1}FN\u{1}";
369
370/// Build the Google-Sheets-style "wrong number of arguments" diagnostic. The
371/// function name is left as [`FN_NAME_PLACEHOLDER`] for the dispatch site to
372/// fill in. Example (min == max == 3, got == 0):
373/// `"Wrong number of arguments to DATE. Expected 3 arguments, but got 0 arguments."`
374fn arity_message(min: usize, max: usize, got: usize) -> String {
375    fn plural(n: usize) -> &'static str {
376        if n == 1 { "" } else { "s" }
377    }
378    let expected = if min == max {
379        format!("{min} argument{}", plural(min))
380    } else if max == usize::MAX {
381        format!("at least {min} argument{}", plural(min))
382    } else {
383        format!("between {min} and {max} arguments")
384    };
385    format!(
386        "Wrong number of arguments to {FN_NAME_PLACEHOLDER}. Expected {expected}, but got {got} argument{}.",
387        plural(got)
388    )
389}
390
391/// Validate argument count for eager functions (args already evaluated to `&[Value]`).
392/// Returns `Some(Value::ErrorMsg(ErrorKind::NA, <message>))` if the count is out
393/// of range (matches Google Sheets / Excel behaviour for wrong argument count).
394/// The error *code* is unchanged (`#N/A`); only an additive diagnostic message
395/// is attached.
396pub fn check_arity(args: &[Value], min: usize, max: usize) -> Option<Value> {
397    check_arity_len(args.len(), min, max)
398}
399
400/// Validate argument count for lazy functions (args are `&[Expr]`).
401/// Returns `Some(Value::ErrorMsg(ErrorKind::NA, <message>))` if the count is out
402/// of range.
403pub fn check_arity_len(count: usize, min: usize, max: usize) -> Option<Value> {
404    if count < min || count > max {
405        Some(Value::ErrorMsg(ErrorKind::NA, arity_message(min, max, count)))
406    } else {
407        None
408    }
409}
410
411// ── Tests ─────────────────────────────────────────────────────────────────
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn list_functions_matches_registry() {
419        let registry = Registry::new();
420        let listed: Vec<(&str, &FunctionMeta)> = registry.list_functions().collect();
421        assert!(!listed.is_empty(), "registry should expose at least one function");
422        // Every listed name must be resolvable — catches metadata/functions map skew
423        for (name, _meta) in &listed {
424            assert!(
425                registry.get(name).is_some(),
426                "listed function {name} not found via get()"
427            );
428        }
429        // metadata count == listed count (no orphaned metadata entries)
430        assert_eq!(listed.len(), registry.metadata.len());
431    }
432}