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 logical;
8pub mod lookup;
9pub mod math;
10pub mod operator;
11pub mod parser;
12pub mod statistical;
13pub mod text;
14pub mod web;
15
16use std::collections::HashMap;
17use crate::eval::context::Context;
18use crate::eval::resolver::Resolver;
19use crate::parser::ast::Expr;
20use crate::types::{ErrorKind, Value};
21
22// ── EvalCtx ───────────────────────────────────────────────────────────────
23
24/// Bundles the variable context, function registry, and reference resolver
25/// for use during evaluation. Passed to lazy functions so they can recursively
26/// evaluate sub-expressions.
27///
28/// References that are not bound as local variables (e.g. a LAMBDA parameter)
29/// are read through `resolver`; see [`crate::Resolver`]. When `resolver` is
30/// `None` (the default, via [`EvalCtx::new`]) every such reference reads as
31/// [`Value::Empty`], preserving the historical contract of
32/// [`crate::Engine::evaluate`].
33pub struct EvalCtx<'r> {
34    pub ctx: Context,
35    pub registry: &'r Registry,
36    /// Resolver for references not bound as local variables. `None` ⇒ such
37    /// references read as [`Value::Empty`] (the historical
38    /// [`crate::Engine::evaluate`] contract).
39    pub resolver: Option<&'r mut dyn Resolver>,
40}
41
42impl<'r> EvalCtx<'r> {
43    /// Build an `EvalCtx` with no resolver: unbound references read as
44    /// [`Value::Empty`]. Use [`EvalCtx::with_resolver`] to supply real
45    /// workbook semantics.
46    pub fn new(ctx: Context, registry: &'r Registry) -> Self {
47        Self { ctx, registry, resolver: None }
48    }
49
50    /// Build an `EvalCtx` that resolves references through `resolver`.
51    pub fn with_resolver(
52        ctx: Context,
53        registry: &'r Registry,
54        resolver: &'r mut dyn Resolver,
55    ) -> Self {
56        Self { ctx, registry, resolver: Some(resolver) }
57    }
58
59    /// Resolve a reference that was not bound as a local variable, delegating
60    /// to [`EvalCtx::resolver`] when present and falling back to
61    /// [`Value::Empty`] otherwise.
62    pub fn resolve_ref(&mut self, r: &crate::parser::refs::Ref) -> Value {
63        match self.resolver {
64            Some(ref mut res) => res.resolve(r),
65            None => Value::Empty,
66        }
67    }
68}
69
70// ── Function kinds ─────────────────────────────────────────────────────────
71
72/// A function that receives pre-evaluated arguments.
73/// Argument errors are caught before dispatch — the slice never contains `Value::Error`.
74pub type EagerFn = fn(&[Value]) -> Value;
75
76/// A function that receives raw AST nodes and controls its own evaluation order.
77/// Used for short-circuit operators like `IF`, `AND`, `OR`.
78pub type LazyFn  = fn(&[Expr], &mut EvalCtx<'_>) -> Value;
79
80#[derive(Clone)]
81pub enum FunctionKind {
82    Eager(EagerFn),
83    Lazy(LazyFn),
84}
85
86// ── FunctionMeta ──────────────────────────────────────────────────────────
87
88/// Metadata for a user-facing spreadsheet function.
89/// Co-located with the registration call so it can never drift.
90#[derive(Debug, Clone)]
91pub struct FunctionMeta {
92    pub category: &'static str,
93    pub signature: &'static str,
94    pub description: &'static str,
95}
96
97/// A metadata entry returned by `Registry::get_metadata()`.
98pub struct FunctionMetaEntry<'a> {
99    pub name: &'a str,
100    pub meta: &'a FunctionMeta,
101}
102
103// ── Registry ──────────────────────────────────────────────────────────────
104
105/// The runtime registry of built-in and user-registered spreadsheet functions.
106pub struct Registry {
107    pub functions: HashMap<String, FunctionKind>,
108    pub metadata: HashMap<String, FunctionMeta>,
109}
110
111impl Registry {
112    pub fn new() -> Self {
113        let mut r = Self { functions: HashMap::new(), metadata: HashMap::new() };
114        math::register_math(&mut r);
115        logical::register_logical(&mut r);
116        text::register_text(&mut r);
117        financial::register_financial(&mut r);
118        statistical::register_statistical(&mut r);
119        operator::register_operator(&mut r);
120        date::register_date(&mut r);
121        parser::register_parser(&mut r);
122        engineering::register_engineering(&mut r);
123        filter::register_filter(&mut r);
124        array::register_array(&mut r);
125        database::register_database(&mut r);
126        lookup::register_lookup(&mut r);
127        web::register_web(&mut r);
128        r
129    }
130
131    /// Register a user-facing eager function with metadata.
132    /// Appears in `list_functions()`.
133    /// Panics if `name` is already registered (duplicate registration).
134    pub fn register_eager(&mut self, name: &str, f: EagerFn, meta: FunctionMeta) {
135        let key = name.to_uppercase();
136        assert!(
137            !self.functions.contains_key(&key),
138            "duplicate function registration: '{}'",
139            key
140        );
141        self.functions.insert(key.clone(), FunctionKind::Eager(f));
142        self.metadata.insert(key, meta);
143    }
144
145    /// Register a user-facing lazy function with metadata.
146    /// Appears in `list_functions()`.
147    /// Panics if `name` is already registered (duplicate registration).
148    pub fn register_lazy(&mut self, name: &str, f: LazyFn, meta: FunctionMeta) {
149        let key = name.to_uppercase();
150        assert!(
151            !self.functions.contains_key(&key),
152            "duplicate function registration: '{}'",
153            key
154        );
155        self.functions.insert(key.clone(), FunctionKind::Lazy(f));
156        self.metadata.insert(key, meta);
157    }
158
159    /// Register `alias` as an alternate name for `canonical`.
160    /// The alias shares the same handler but does NOT appear in function metadata
161    /// (it will not show up in `list_functions()` or autocomplete).
162    /// Panics if `alias` is already registered or `canonical` is not yet registered.
163    pub fn register_alias(&mut self, alias: &str, canonical: &str) {
164        let alias_key = alias.to_uppercase();
165        let canonical_key = canonical.to_uppercase();
166        assert!(
167            !self.functions.contains_key(&alias_key),
168            "duplicate function registration: '{}'",
169            alias_key
170        );
171        let kind = self
172            .functions
173            .get(&canonical_key)
174            .unwrap_or_else(|| {
175                panic!(
176                    "register_alias: canonical '{}' must be registered before alias '{}'",
177                    canonical_key, alias_key
178                )
179            })
180            .clone();
181        self.functions.insert(alias_key, kind);
182        // Intentionally no metadata entry — aliases are not user-facing
183    }
184
185    /// Register an internal/compiler-only eager function without metadata.
186    /// Never appears in `list_functions()`.
187    pub fn register_internal(&mut self, name: &str, f: EagerFn) {
188        self.functions.insert(name.to_uppercase(), FunctionKind::Eager(f));
189    }
190
191    /// Register an internal/compiler-only lazy function without metadata.
192    /// Never appears in `list_functions()`.
193    pub fn register_internal_lazy(&mut self, name: &str, f: LazyFn) {
194        self.functions.insert(name.to_uppercase(), FunctionKind::Lazy(f));
195    }
196
197    pub fn get(&self, name: &str) -> Option<&FunctionKind> {
198        self.functions.get(&name.to_uppercase())
199    }
200
201    /// Iterate all user-facing functions with their metadata.
202    /// The registry is the single source of truth — this can never drift.
203    pub fn list_functions(&self) -> impl Iterator<Item = (&str, &FunctionMeta)> {
204        self.metadata.iter().map(|(k, v)| (k.as_str(), v))
205    }
206
207    /// Return all function metadata entries as a Vec of named structs.
208    /// Used for inspection (e.g. counting functions, verifying aliases are absent).
209    pub fn get_metadata(&self) -> Vec<FunctionMetaEntry<'_>> {
210        self.metadata
211            .iter()
212            .map(|(k, v)| FunctionMetaEntry { name: k.as_str(), meta: v })
213            .collect()
214    }
215
216    /// Return all user-facing function names (from metadata, not aliases).
217    pub fn metadata_names(&self) -> Vec<String> {
218        self.metadata.keys().cloned().collect()
219    }
220}
221
222impl Registry {
223    /// Volatile functions — outputs change on every evaluation.
224    /// Excluded from conformance fixtures; covered by property tests instead.
225    pub const VOLATILE_FUNCTIONS: &'static [&'static str] = &[
226        "RAND", "RANDARRAY", "NOW", "TODAY", "RANDBETWEEN",
227    ];
228}
229
230impl Default for Registry {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236/// Validate argument count for eager functions (args already evaluated to `&[Value]`).
237/// Returns `Some(Value::Error(ErrorKind::NA))` if the count is out of range
238/// (matches Google Sheets / Excel behaviour for wrong argument count).
239pub fn check_arity(args: &[Value], min: usize, max: usize) -> Option<Value> {
240    if args.len() < min || args.len() > max {
241        Some(Value::Error(ErrorKind::NA))
242    } else {
243        None
244    }
245}
246
247/// Validate argument count for lazy functions (args are `&[Expr]`).
248/// Returns `Some(Value::Error(ErrorKind::NA))` if the count is out of range.
249pub fn check_arity_len(count: usize, min: usize, max: usize) -> Option<Value> {
250    if count < min || count > max {
251        Some(Value::Error(ErrorKind::NA))
252    } else {
253        None
254    }
255}
256
257// ── Tests ─────────────────────────────────────────────────────────────────
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn list_functions_matches_registry() {
265        let registry = Registry::new();
266        let listed: Vec<(&str, &FunctionMeta)> = registry.list_functions().collect();
267        assert!(!listed.is_empty(), "registry should expose at least one function");
268        // Every listed name must be resolvable — catches metadata/functions map skew
269        for (name, _meta) in &listed {
270            assert!(
271                registry.get(name).is_some(),
272                "listed function {name} not found via get()"
273            );
274        }
275        // metadata count == listed count (no orphaned metadata entries)
276        assert_eq!(listed.len(), registry.metadata.len());
277    }
278}