Skip to main content

formualizer_eval/
function.rs

1//! formualizer-eval/src/function.rs
2// New home for the core `Function` trait and its capability flags.
3
4use core::panic;
5
6use crate::{
7    args::ArgSchema,
8    function_contract::{FunctionDependencyContract, FunctionSemanticContract},
9    traits::ArgumentHandle,
10};
11use formualizer_common::{ExcelError, LiteralValue};
12use formualizer_parse::parser::ReferenceType;
13
14/// Function code is an unwind boundary; engine bookkeeping stays outside it.
15pub(crate) fn catch_function_panic<T>(
16    name: &str,
17    run: impl FnOnce() -> T,
18) -> Result<T, ExcelError> {
19    catch_external_panic(run).map_err(|payload| ExcelError::from_function_panic(name, &*payload))
20}
21
22/// The only raw unwind catch used by execution/metadata/resolver adapters.
23pub(crate) fn catch_external_panic<T>(
24    run: impl FnOnce() -> T,
25) -> Result<T, Box<dyn std::any::Any + Send>> {
26    std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
27}
28
29/// Dispatch supports both ordinary values and optional reference results.
30pub(crate) trait PanicResult {
31    fn panicked(error: ExcelError) -> Self;
32}
33impl<T> PanicResult for Result<T, ExcelError> {
34    fn panicked(error: ExcelError) -> Self {
35        Err(error)
36    }
37}
38impl<T> PanicResult for Option<Result<T, ExcelError>> {
39    fn panicked(error: ExcelError) -> Self {
40        Some(Err(error))
41    }
42}
43
44/// One-pass result for a function used where either a reference or a value is valid.
45#[doc(hidden)]
46#[derive(Clone)]
47pub enum FunctionResolution<'a> {
48    Reference(ReferenceType),
49    ReferenceError(ExcelError),
50    Value(crate::traits::CalcValue<'a>),
51}
52
53pub(crate) fn resolution_to_reference(
54    result: Result<FunctionResolution<'_>, ExcelError>,
55) -> Option<Result<ReferenceType, ExcelError>> {
56    match result {
57        Ok(FunctionResolution::Reference(reference)) => Some(Ok(reference)),
58        Ok(FunctionResolution::ReferenceError(error)) | Err(error) => Some(Err(error)),
59        Ok(FunctionResolution::Value(crate::traits::CalcValue::Scalar(LiteralValue::Error(
60            error,
61        )))) => Some(Err(error)),
62        Ok(FunctionResolution::Value(_)) => None,
63    }
64}
65
66bitflags::bitflags! {
67    /// Describes the capabilities and properties of a function.
68    ///
69    /// This allows the engine to select optimal evaluation paths (e.g., vectorized,
70    /// parallel, GPU) and to enforce semantic contracts at compile time.
71    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
72    pub struct FnCaps: u32 {
73        // --- Semantics ---
74        /// The function always produces the same output for the same input and has no
75        /// side effects. This is the default for most functions.
76        const PURE          = 0b0000_0000_0001;
77        /// The function's output can change even with the same inputs (e.g., `RAND()`,
78        /// `NOW()`). Volatile functions are re-evaluated on every sheet change.
79        const VOLATILE      = 0b0000_0000_0010;
80
81        // --- Shape / Evaluation Strategy ---
82        /// The function reduces a range of inputs to a single value (e.g., `SUM`, `AVERAGE`).
83        const REDUCTION     = 0b0000_0000_0100;
84        /// The function operates on each element of its input ranges independently
85        /// (e.g., `SIN`, `ABS`).
86        const ELEMENTWISE   = 0b0000_0000_1000;
87        /// The function operates on a sliding window over its input (e.g., `MOVING_AVERAGE`).
88        const WINDOWED      = 0b0000_0001_0000;
89        /// The function performs a lookup or search operation (e.g., `VLOOKUP`).
90        const LOOKUP        = 0b0000_0010_0000;
91
92        // --- Input Data Types ---
93        /// The function primarily operates on numbers. The engine can prepare
94        /// optimized numeric stripes (`&[f64]`) for it.
95        const NUMERIC_ONLY  = 0b0000_0100_0000;
96        /// The function primarily operates on booleans.
97        const BOOL_ONLY     = 0b0000_1000_0000;
98
99        // --- Backend Optimizations ---
100        /// The function has an implementation suitable for SIMD vectorization.
101        const SIMD_OK       = 0b0001_0000_0000;
102        /// The function can process input as a stream, without materializing the
103        /// entire range in memory.
104        const STREAM_OK     = 0b0010_0000_0000;
105        /// The function has a GPU-accelerated implementation.
106        const GPU_OK        = 0b0100_0000_0000;
107
108        // --- Reference semantics ---
109        /// The function can return a reference (to a cell/range/table) when
110        /// evaluated in a reference context. When used in a value context,
111        /// engines may materialize the reference to a `LiteralValue`.
112    const RETURNS_REFERENCE = 0b1000_0000_0000;
113
114    // --- Planning / Interpreter parallelism hints ---
115    /// The function enforces left-to-right evaluation and early-exit semantics.
116    /// The planner must not evaluate arguments in parallel nor reorder them.
117    const SHORT_CIRCUIT  = 0b0001_0000_0000_0000;
118    /// It is safe and potentially profitable to evaluate arguments in parallel.
119    /// The engine should still fold results in argument order for determinism.
120    const PARALLEL_ARGS  = 0b0010_0000_0000_0000;
121    /// It is safe to chunk and process input windows in parallel (e.g., SUMIFS).
122    /// It is safe to chunk and process input windows in parallel (e.g., SUMIFS).
123    const PARALLEL_CHUNKS= 0b0100_0000_0000_0000;
124    /// Function has dynamic dependencies determined at runtime (e.g. INDIRECT, OFFSET).
125    const DYNAMIC_DEPENDENCY = 0b1000_0000_0000_0000;
126    /// Function establishes lexical bindings or evaluates a local call environment.
127    const LOCAL_ENVIRONMENT = 0b0001_0000_0000_0000_0000;
128    /// Function can produce a multi-cell dynamic-array result.
129    const MAY_SPILL = 0b0010_0000_0000_0000_0000;
130    }
131}
132
133/// Revised, object-safe trait for all Excel-style functions.
134///
135/// This trait uses a capability-based model (`FnCaps`) to declare function
136/// properties, enabling the evaluation engine to select the most optimal
137/// execution path (e.g., scalar, vectorized, parallel).
138/// Engine-internal identity of a built-in function with a family kernel.
139#[doc(hidden)]
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum FamilyKernel {
143    Sum,
144    Average,
145    /// `IF`: lifted element-wise (P2-M3), not a range kernel.
146    If,
147    /// `SUMIF(S)`, `COUNTIF(S)`, `AVERAGEIF(S)`: a family run memoizes by
148    /// its varying arguments (the result is a function of them and of the
149    /// absolute ranges).
150    CriteriaAggregate,
151    /// `VLOOKUP`, `HLOOKUP`, `MATCH`: memoized like `CriteriaAggregate`.
152    Lookup,
153    /// Scalar builtins the elementwise lift runs on typed lanes (P2-M3).
154    Round,
155    Abs,
156    Min,
157    Max,
158    And,
159    Or,
160    IfError,
161    /// `COUNT`: a windowed range kernel.
162    Count,
163    /// Program 3: type tests and date parts on typed lanes (a clean
164    /// operand's result without the walk).
165    IsNumber,
166    IsText,
167    IsLogical,
168    IsBlank,
169    IsError,
170    IsErr,
171    IsNa,
172    Year,
173    Month,
174    Day,
175    Weekday,
176}
177
178impl FamilyKernel {
179    pub(crate) fn name(self) -> &'static str {
180        match self {
181            Self::Sum => "SUM",
182            Self::Average => "AVERAGE",
183            Self::If => "IF",
184            Self::CriteriaAggregate => "CRITERIA",
185            Self::Lookup => "LOOKUP",
186            Self::Round => "ROUND",
187            Self::Abs => "ABS",
188            Self::Min => "MIN",
189            Self::Max => "MAX",
190            Self::And => "AND",
191            Self::Or => "OR",
192            Self::IfError => "IFERROR",
193            Self::Count => "COUNT",
194            Self::IsNumber => "ISNUMBER",
195            Self::IsText => "ISTEXT",
196            Self::IsLogical => "ISLOGICAL",
197            Self::IsBlank => "ISBLANK",
198            Self::IsError => "ISERROR",
199            Self::IsErr => "ISERR",
200            Self::IsNa => "ISNA",
201            Self::Year => "YEAR",
202            Self::Month => "MONTH",
203            Self::Day => "DAY",
204            Self::Weekday => "WEEKDAY",
205        }
206    }
207}
208
209pub trait Function: Send + Sync + 'static {
210    /// Capability flags for this function
211    fn caps(&self) -> FnCaps {
212        FnCaps::PURE
213    }
214
215    /// Engine-internal: the family kernel that reproduces this function
216    /// exactly over a family run. Only the built-in implementations return
217    /// `Some`; an override registered under the same name keeps `None`, so
218    /// the engine evaluates it through `eval`.
219    #[doc(hidden)]
220    fn family_kernel(&self) -> Option<FamilyKernel> {
221        None
222    }
223
224    fn name(&self) -> &'static str;
225    fn namespace(&self) -> &'static str {
226        ""
227    }
228    fn min_args(&self) -> usize {
229        0
230    }
231    fn variadic(&self) -> bool {
232        false
233    }
234    fn volatile(&self) -> bool {
235        crate::external_callbacks::dispatch_metadata(self, FnCaps::empty, || self.caps())
236            .contains(FnCaps::VOLATILE)
237    }
238    fn arg_schema(&self) -> &'static [ArgSchema] {
239        if crate::external_callbacks::dispatch_metadata(self, || 0, || self.min_args()) > 0 {
240            panic!("Non-zero min_args must have a valid arg_schema");
241        } else {
242            &[]
243        }
244    }
245
246    /// Optional list of additional alias names (case-insensitive) that should resolve to this
247    /// function. Default: empty slice. Implementors can override to expose legacy names.
248    /// Returned slice must have 'static lifetime (typically a static array reference).
249    fn aliases(&self) -> &'static [&'static str] {
250        &[]
251    }
252
253    /// Optional dependency contract for passive planning/FormulaPlane analysis.
254    ///
255    /// The default is deliberately conservative: functions that do not opt in
256    /// must not receive dependency-summary optimization. Implementations should
257    /// return `Some` only for arities and argument roles they can describe
258    /// without under-approximating dependencies.
259    fn dependency_contract(&self, _arity: usize) -> Option<FunctionDependencyContract> {
260        None
261    }
262
263    /// Promise to report every known unavailable or unverified executed mode
264    /// through FunctionContext. This is independent of PURE/optimization flags
265    /// and does not certify numerical equivalence. The promise must remain
266    /// side-effect free and stable for this provider instance's lifetime.
267    /// Unattested providers default
268    /// to unknown coverage; crate-owned registrations have a sealed obligation.
269    fn reports_known_limitations(&self) -> bool {
270        false
271    }
272
273    /// Explicit semantic classification for this call arity.
274    ///
275    /// The public default is intentionally untrusted. The registry supplies a
276    /// sealed default only for crate-owned builtin registration.
277    fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
278        None
279    }
280
281    #[inline]
282    fn function_salt(&self) -> u64 {
283        // Stable hash of function name + namespace
284        let full_name =
285            if crate::external_callbacks::dispatch_metadata(self, || "", || self.namespace())
286                .is_empty()
287            {
288                crate::external_callbacks::dispatch_metadata(self, || "", || self.name())
289                    .to_string()
290            } else {
291                format!(
292                    "{}::{}",
293                    crate::external_callbacks::dispatch_metadata(self, || "", || self.namespace()),
294                    crate::external_callbacks::dispatch_metadata(self, || "", || self.name())
295                )
296            };
297        crate::rng::fnv1a64(full_name.as_bytes())
298    }
299
300    /// Derive an eval-internal scalar format annotation for this call.
301    /// Functions drop annotations by default; selection and temporal constructors override it.
302    fn propagate_format(
303        &self,
304        result: &crate::traits::CalcValue<'_>,
305    ) -> Option<crate::format::FormatId> {
306        let _ = result;
307        None
308    }
309
310    fn apply_format_propagation<'a>(
311        &self,
312        result: crate::traits::CalcValue<'a>,
313    ) -> crate::traits::CalcValue<'a> {
314        let format = crate::external_callbacks::dispatch_metadata(
315            self,
316            || None,
317            || self.propagate_format(&result),
318        );
319        result.with_format(format)
320    }
321
322    /// The unified evaluation path.
323    ///
324    /// This method replaces the separate scalar, fold, and map paths.
325    /// Functions use the provided `ArgumentHandle`s to access inputs as either
326    /// scalars or `RangeView`s (Arrow-backed virtual ranges).
327    fn eval<'a, 'b, 'c>(
328        &self,
329        args: &'c [ArgumentHandle<'a, 'b>],
330        ctx: &dyn crate::traits::FunctionContext<'b>,
331    ) -> Result<crate::traits::CalcValue<'b>, ExcelError>;
332
333    /// Optional reference result path. Only called by the interpreter/engine
334    /// when the callsite expects a reference (e.g., range combinators, by-ref
335    /// argument positions, or spill sources).
336    ///
337    /// Default implementation returns `None`, indicating the function does not
338    /// support returning references. Functions that set `RETURNS_REFERENCE`
339    /// should override this.
340    fn eval_reference<'a, 'b, 'c>(
341        &self,
342        _args: &'c [ArgumentHandle<'a, 'b>],
343        _ctx: &dyn crate::traits::FunctionContext<'b>,
344    ) -> Option<Result<formualizer_parse::parser::ReferenceType, ExcelError>> {
345        None
346    }
347
348    /// Resolve a reference-capable function while preserving scalar-selector caching.
349    ///
350    /// The fallback deliberately re-enters the caller's ordinary value path. That
351    /// preserves dispatch validation and array lifting for existing functions whose
352    /// `eval_reference` declines a particular argument shape. Array-valued selectors
353    /// may be evaluated once by each path.
354    fn resolve_reference_or_value<'a, 'b, 'c>(
355        &self,
356        args: &'c [ArgumentHandle<'a, 'b>],
357        ctx: &dyn crate::traits::FunctionContext<'b>,
358        value_fallback: &dyn Fn() -> Result<crate::traits::CalcValue<'b>, ExcelError>,
359    ) -> Result<FunctionResolution<'b>, ExcelError> {
360        match self.eval_reference(args, ctx) {
361            Some(Ok(reference)) => Ok(FunctionResolution::Reference(reference)),
362            Some(Err(error)) => Ok(FunctionResolution::ReferenceError(error)),
363            None => value_fallback().map(FunctionResolution::Value),
364        }
365    }
366
367    /// Dispatch to the unified evaluation path with automatic argument validation.
368    fn dispatch<'a, 'b, 'c>(
369        &self,
370        args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
371        ctx: &dyn crate::traits::FunctionContext<'b>,
372    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
373        // Short-circuit functions (IF/IFS/CHOOSE/SWITCH/AND/OR, ...) evaluate
374        // their arguments lazily inside `eval`; eagerly materializing every
375        // argument here would execute reads in untaken branches (defeating the
376        // documented short-circuit semantics) and double-evaluate taken ones.
377        // Their schemas are Any-kind with no per-arg coercion, so per-argument
378        // validation cannot fail; only the min-arity check is meaningful.
379        // (LET/LAMBDA already bypass validation via `dispatch` overrides for
380        // the same reason.)
381        if crate::external_callbacks::dispatch_metadata(self, FnCaps::empty, || self.caps())
382            .contains(FnCaps::SHORT_CIRCUIT)
383        {
384            if args.len()
385                < crate::external_callbacks::dispatch_metadata(self, || 0, || self.min_args())
386            {
387                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
388                    ExcelError::new(formualizer_common::ExcelErrorKind::Value).with_message(
389                        format!(
390                            "Too few arguments: expected at least {}, got {}",
391                            crate::external_callbacks::dispatch_metadata(
392                                self,
393                                || 0,
394                                || self.min_args()
395                            ),
396                            args.len()
397                        ),
398                    ),
399                )));
400            }
401            return self
402                .eval(args, ctx)
403                .map(|result| self.apply_format_propagation(result));
404        }
405
406        // Central argument validation (includes min-arity check)
407        {
408            use crate::args::{ValidationOptions, validate_and_prepare_inline};
409            let schema = crate::external_callbacks::dispatch_metadata(
410                self,
411                || &[] as &'static [ArgSchema],
412                || self.arg_schema(),
413            );
414            if let Err(e) = validate_and_prepare_inline(
415                args,
416                schema,
417                ValidationOptions {
418                    warn_only: false,
419                    min_args: crate::external_callbacks::dispatch_metadata(
420                        self,
421                        || 0,
422                        || self.min_args(),
423                    ),
424                },
425            ) {
426                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
427            }
428        }
429
430        self.eval(args, ctx)
431            .map(|result| self.apply_format_propagation(result))
432    }
433}
434
435#[cfg(test)]
436mod argument_preparation_tests {
437    use super::*;
438    use crate::{test_workbook::TestWorkbook, traits::CalcValue};
439    use formualizer_common::ExcelErrorKind;
440    use std::sync::{
441        Arc,
442        atomic::{AtomicUsize, Ordering},
443    };
444
445    struct Count(Arc<AtomicUsize>);
446    impl Function for Count {
447        fn name(&self) -> &'static str {
448            "ARGCOUNT"
449        }
450        fn eval<'a, 'b, 'c>(
451            &self,
452            _args: &'c [ArgumentHandle<'a, 'b>],
453            _ctx: &dyn crate::traits::FunctionContext<'b>,
454        ) -> Result<CalcValue<'b>, ExcelError> {
455            self.0.fetch_add(1, Ordering::SeqCst);
456            Ok(CalcValue::Scalar(LiteralValue::Number(-7.0)))
457        }
458    }
459
460    #[test]
461    fn nested_validation_memo_is_linear() -> Result<(), Box<dyn std::error::Error>> {
462        crate::builtins::load_builtins();
463        let calls = Arc::new(AtomicUsize::new(0));
464        let wb = TestWorkbook::new().with_function(Arc::new(Count(Arc::clone(&calls))));
465        let interp = wb.interpreter();
466        let formula = format!("={}ARGCOUNT(){}", "ABS(".repeat(12), ")".repeat(12));
467        let ast = formualizer_parse::parser::parse(&formula)?;
468        for expected in 1..=2 {
469            assert_eq!(
470                interp.evaluate_ast(&ast)?.into_literal(),
471                LiteralValue::Number(7.0)
472            );
473            assert_eq!(
474                calls.load(Ordering::SeqCst),
475                expected,
476                "one leaf evaluation per recalc, not 2^depth"
477            );
478        }
479        Ok(())
480    }
481
482    #[test]
483    fn lazy_omitted_and_validation_errors_are_preserved() -> Result<(), Box<dyn std::error::Error>>
484    {
485        crate::builtins::load_builtins();
486        let calls = Arc::new(AtomicUsize::new(0));
487        let wb = TestWorkbook::new().with_function(Arc::new(Count(Arc::clone(&calls))));
488        let interp = wb.interpreter();
489        for (formula, expected) in [
490            ("=IF(TRUE,7,ARGCOUNT())", LiteralValue::Number(7.0)),
491            ("=IF(FALSE,ARGCOUNT(),7)", LiteralValue::Number(7.0)),
492            ("=IFERROR(7,ARGCOUNT())", LiteralValue::Number(7.0)),
493            ("=IFERROR(1/0,7)", LiteralValue::Number(7.0)),
494            (
495                "=CHOOSE(2,ARGCOUNT(),7,ARGCOUNT())",
496                LiteralValue::Number(7.0),
497            ),
498            ("=IF(TRUE,,ARGCOUNT())", LiteralValue::Number(0.0)),
499            ("=CHOOSE(2,ARGCOUNT(),)", LiteralValue::Number(0.0)),
500        ] {
501            let ast = formualizer_parse::parser::parse(formula)?;
502            assert_eq!(
503                interp.evaluate_ast(&ast)?.into_literal(),
504                expected,
505                "{formula}"
506            );
507        }
508        assert_eq!(
509            calls.load(Ordering::SeqCst),
510            0,
511            "untaken branches stay lazy"
512        );
513        for (formula, kind) in [
514            ("=ROUND(1/0,ARGCOUNT())", ExcelErrorKind::Div),
515            ("=ROUND(NA(),1/0)", ExcelErrorKind::Na),
516            ("=ROUND(NA())", ExcelErrorKind::Value),
517            ("=ABS(\"not a number\")", ExcelErrorKind::Value),
518        ] {
519            let ast = formualizer_parse::parser::parse(formula)?;
520            let result = interp.evaluate_ast(&ast)?.into_literal();
521            assert!(
522                matches!(result, LiteralValue::Error(ref e) if e.kind == kind),
523                "{formula}: {result:?}"
524            );
525        }
526        assert_eq!(
527            calls.load(Ordering::SeqCst),
528            0,
529            "validation stops at the first error"
530        );
531        Ok(())
532    }
533}