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