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::parser::ast::Expr;
19use crate::types::{ErrorKind, Value};
20
21// ── EvalCtx ───────────────────────────────────────────────────────────────
22
23/// Bundles the variable context and function registry for use during evaluation.
24/// Passed to lazy functions so they can recursively evaluate sub-expressions.
25pub struct EvalCtx<'r> {
26    pub ctx: Context,
27    pub registry: &'r Registry,
28}
29
30impl<'r> EvalCtx<'r> {
31    pub fn new(ctx: Context, registry: &'r Registry) -> Self {
32        Self { ctx, registry }
33    }
34}
35
36// ── Function kinds ─────────────────────────────────────────────────────────
37
38/// A function that receives pre-evaluated arguments.
39/// Argument errors are caught before dispatch — the slice never contains `Value::Error`.
40pub type EagerFn = fn(&[Value]) -> Value;
41
42/// A function that receives raw AST nodes and controls its own evaluation order.
43/// Used for short-circuit operators like `IF`, `AND`, `OR`.
44pub type LazyFn  = fn(&[Expr], &mut EvalCtx<'_>) -> Value;
45
46pub enum FunctionKind {
47    Eager(EagerFn),
48    Lazy(LazyFn),
49}
50
51// ── FunctionMeta ──────────────────────────────────────────────────────────
52
53/// Metadata for a user-facing spreadsheet function.
54/// Co-located with the registration call so it can never drift.
55#[derive(Debug)]
56pub struct FunctionMeta {
57    pub category: &'static str,
58    pub signature: &'static str,
59    pub description: &'static str,
60}
61
62// ── Registry ──────────────────────────────────────────────────────────────
63
64/// The runtime registry of built-in and user-registered spreadsheet functions.
65pub struct Registry {
66    functions: HashMap<String, FunctionKind>,
67    metadata: HashMap<String, FunctionMeta>,
68}
69
70impl Registry {
71    pub fn new() -> Self {
72        let mut r = Self { functions: HashMap::new(), metadata: HashMap::new() };
73        math::register_math(&mut r);
74        logical::register_logical(&mut r);
75        text::register_text(&mut r);
76        financial::register_financial(&mut r);
77        statistical::register_statistical(&mut r);
78        operator::register_operator(&mut r);
79        date::register_date(&mut r);
80        parser::register_parser(&mut r);
81        engineering::register_engineering(&mut r);
82        filter::register_filter(&mut r);
83        array::register_array(&mut r);
84        database::register_database(&mut r);
85        lookup::register_lookup(&mut r);
86        web::register_web(&mut r);
87        r
88    }
89
90    /// Register a user-facing eager function with metadata.
91    /// Appears in `list_functions()`.
92    pub fn register_eager(&mut self, name: &str, f: EagerFn, meta: FunctionMeta) {
93        let key = name.to_uppercase();
94        self.functions.insert(key.clone(), FunctionKind::Eager(f));
95        self.metadata.insert(key, meta);
96    }
97
98    /// Register a user-facing lazy function with metadata.
99    /// Appears in `list_functions()`.
100    pub fn register_lazy(&mut self, name: &str, f: LazyFn, meta: FunctionMeta) {
101        let key = name.to_uppercase();
102        self.functions.insert(key.clone(), FunctionKind::Lazy(f));
103        self.metadata.insert(key, meta);
104    }
105
106    /// Register an internal/compiler-only eager function without metadata.
107    /// Never appears in `list_functions()`.
108    pub fn register_internal(&mut self, name: &str, f: EagerFn) {
109        self.functions.insert(name.to_uppercase(), FunctionKind::Eager(f));
110    }
111
112    /// Register an internal/compiler-only lazy function without metadata.
113    /// Never appears in `list_functions()`.
114    pub fn register_internal_lazy(&mut self, name: &str, f: LazyFn) {
115        self.functions.insert(name.to_uppercase(), FunctionKind::Lazy(f));
116    }
117
118    pub fn get(&self, name: &str) -> Option<&FunctionKind> {
119        self.functions.get(&name.to_uppercase())
120    }
121
122    /// Iterate all user-facing functions with their metadata.
123    /// The registry is the single source of truth — this can never drift.
124    pub fn list_functions(&self) -> impl Iterator<Item = (&str, &FunctionMeta)> {
125        self.metadata.iter().map(|(k, v)| (k.as_str(), v))
126    }
127}
128
129impl Default for Registry {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135/// Validate argument count for eager functions (args already evaluated to `&[Value]`).
136/// Returns `Some(Value::Error(ErrorKind::NA))` if the count is out of range
137/// (matches Google Sheets / Excel behaviour for wrong argument count).
138pub fn check_arity(args: &[Value], min: usize, max: usize) -> Option<Value> {
139    if args.len() < min || args.len() > max {
140        Some(Value::Error(ErrorKind::NA))
141    } else {
142        None
143    }
144}
145
146/// Validate argument count for lazy functions (args are `&[Expr]`).
147/// Returns `Some(Value::Error(ErrorKind::NA))` if the count is out of range.
148pub fn check_arity_len(count: usize, min: usize, max: usize) -> Option<Value> {
149    if count < min || count > max {
150        Some(Value::Error(ErrorKind::NA))
151    } else {
152        None
153    }
154}
155
156// ── Tests ─────────────────────────────────────────────────────────────────
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn list_functions_matches_registry() {
164        let registry = Registry::new();
165        let listed: Vec<(&str, &FunctionMeta)> = registry.list_functions().collect();
166        assert!(!listed.is_empty(), "registry should expose at least one function");
167        // Every listed name must be resolvable — catches metadata/functions map skew
168        for (name, _meta) in &listed {
169            assert!(
170                registry.get(name).is_some(),
171                "listed function {name} not found via get()"
172            );
173        }
174        // metadata count == listed count (no orphaned metadata entries)
175        assert_eq!(listed.len(), registry.metadata.len());
176    }
177}