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