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/// One-pass result for a function used where either a reference or a value is valid.
15#[doc(hidden)]
16#[derive(Clone)]
17pub enum FunctionResolution<'a> {
18    Reference(ReferenceType),
19    ReferenceError(ExcelError),
20    Value(crate::traits::CalcValue<'a>),
21}
22
23pub(crate) fn resolution_to_reference(
24    result: Result<FunctionResolution<'_>, ExcelError>,
25) -> Option<Result<ReferenceType, ExcelError>> {
26    match result {
27        Ok(FunctionResolution::Reference(reference)) => Some(Ok(reference)),
28        Ok(FunctionResolution::ReferenceError(error)) | Err(error) => Some(Err(error)),
29        Ok(FunctionResolution::Value(crate::traits::CalcValue::Scalar(LiteralValue::Error(
30            error,
31        )))) => Some(Err(error)),
32        Ok(FunctionResolution::Value(_)) => None,
33    }
34}
35
36bitflags::bitflags! {
37    /// Describes the capabilities and properties of a function.
38    ///
39    /// This allows the engine to select optimal evaluation paths (e.g., vectorized,
40    /// parallel, GPU) and to enforce semantic contracts at compile time.
41    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
42    pub struct FnCaps: u32 {
43        // --- Semantics ---
44        /// The function always produces the same output for the same input and has no
45        /// side effects. This is the default for most functions.
46        const PURE          = 0b0000_0000_0001;
47        /// The function's output can change even with the same inputs (e.g., `RAND()`,
48        /// `NOW()`). Volatile functions are re-evaluated on every sheet change.
49        const VOLATILE      = 0b0000_0000_0010;
50
51        // --- Shape / Evaluation Strategy ---
52        /// The function reduces a range of inputs to a single value (e.g., `SUM`, `AVERAGE`).
53        const REDUCTION     = 0b0000_0000_0100;
54        /// The function operates on each element of its input ranges independently
55        /// (e.g., `SIN`, `ABS`).
56        const ELEMENTWISE   = 0b0000_0000_1000;
57        /// The function operates on a sliding window over its input (e.g., `MOVING_AVERAGE`).
58        const WINDOWED      = 0b0000_0001_0000;
59        /// The function performs a lookup or search operation (e.g., `VLOOKUP`).
60        const LOOKUP        = 0b0000_0010_0000;
61
62        // --- Input Data Types ---
63        /// The function primarily operates on numbers. The engine can prepare
64        /// optimized numeric stripes (`&[f64]`) for it.
65        const NUMERIC_ONLY  = 0b0000_0100_0000;
66        /// The function primarily operates on booleans.
67        const BOOL_ONLY     = 0b0000_1000_0000;
68
69        // --- Backend Optimizations ---
70        /// The function has an implementation suitable for SIMD vectorization.
71        const SIMD_OK       = 0b0001_0000_0000;
72        /// The function can process input as a stream, without materializing the
73        /// entire range in memory.
74        const STREAM_OK     = 0b0010_0000_0000;
75        /// The function has a GPU-accelerated implementation.
76        const GPU_OK        = 0b0100_0000_0000;
77
78        // --- Reference semantics ---
79        /// The function can return a reference (to a cell/range/table) when
80        /// evaluated in a reference context. When used in a value context,
81        /// engines may materialize the reference to a `LiteralValue`.
82    const RETURNS_REFERENCE = 0b1000_0000_0000;
83
84    // --- Planning / Interpreter parallelism hints ---
85    /// The function enforces left-to-right evaluation and early-exit semantics.
86    /// The planner must not evaluate arguments in parallel nor reorder them.
87    const SHORT_CIRCUIT  = 0b0001_0000_0000_0000;
88    /// It is safe and potentially profitable to evaluate arguments in parallel.
89    /// The engine should still fold results in argument order for determinism.
90    const PARALLEL_ARGS  = 0b0010_0000_0000_0000;
91    /// It is safe to chunk and process input windows in parallel (e.g., SUMIFS).
92    /// It is safe to chunk and process input windows in parallel (e.g., SUMIFS).
93    const PARALLEL_CHUNKS= 0b0100_0000_0000_0000;
94    /// Function has dynamic dependencies determined at runtime (e.g. INDIRECT, OFFSET).
95    const DYNAMIC_DEPENDENCY = 0b1000_0000_0000_0000;
96    /// Function establishes lexical bindings or evaluates a local call environment.
97    const LOCAL_ENVIRONMENT = 0b0001_0000_0000_0000_0000;
98    /// Function can produce a multi-cell dynamic-array result.
99    const MAY_SPILL = 0b0010_0000_0000_0000_0000;
100    }
101}
102
103/// Revised, object-safe trait for all Excel-style functions.
104///
105/// This trait uses a capability-based model (`FnCaps`) to declare function
106/// properties, enabling the evaluation engine to select the most optimal
107/// execution path (e.g., scalar, vectorized, parallel).
108/// Engine-internal identity of a built-in function with a family kernel.
109#[doc(hidden)]
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111#[non_exhaustive]
112pub enum FamilyKernel {
113    Sum,
114    Average,
115    /// `IF`: lifted element-wise (P2-M3), not a range kernel.
116    If,
117    /// `SUMIF(S)`, `COUNTIF(S)`, `AVERAGEIF(S)`: a family run memoizes by
118    /// its varying arguments (the result is a function of them and of the
119    /// absolute ranges).
120    CriteriaAggregate,
121    /// `VLOOKUP`, `HLOOKUP`, `MATCH`: memoized like `CriteriaAggregate`.
122    Lookup,
123    /// Scalar builtins the elementwise lift runs on typed lanes (P2-M3).
124    Round,
125    Abs,
126    Min,
127    Max,
128    And,
129    Or,
130    IfError,
131    /// `COUNT`: a windowed range kernel.
132    Count,
133    /// Program 3: type tests and date parts on typed lanes (a clean
134    /// operand's result without the walk).
135    IsNumber,
136    IsText,
137    IsLogical,
138    IsBlank,
139    IsError,
140    IsErr,
141    IsNa,
142    Year,
143    Month,
144    Day,
145    Weekday,
146}
147
148pub trait Function: Send + Sync + 'static {
149    /// Capability flags for this function
150    fn caps(&self) -> FnCaps {
151        FnCaps::PURE
152    }
153
154    /// Engine-internal: the family kernel that reproduces this function
155    /// exactly over a family run. Only the built-in implementations return
156    /// `Some`; an override registered under the same name keeps `None`, so
157    /// the engine evaluates it through `eval`.
158    #[doc(hidden)]
159    fn family_kernel(&self) -> Option<FamilyKernel> {
160        None
161    }
162
163    fn name(&self) -> &'static str;
164    fn namespace(&self) -> &'static str {
165        ""
166    }
167    fn min_args(&self) -> usize {
168        0
169    }
170    fn variadic(&self) -> bool {
171        false
172    }
173    fn volatile(&self) -> bool {
174        self.caps().contains(FnCaps::VOLATILE)
175    }
176    fn arg_schema(&self) -> &'static [ArgSchema] {
177        if self.min_args() > 0 {
178            panic!("Non-zero min_args must have a valid arg_schema");
179        } else {
180            &[]
181        }
182    }
183
184    /// Optional list of additional alias names (case-insensitive) that should resolve to this
185    /// function. Default: empty slice. Implementors can override to expose legacy names.
186    /// Returned slice must have 'static lifetime (typically a static array reference).
187    fn aliases(&self) -> &'static [&'static str] {
188        &[]
189    }
190
191    /// Optional dependency contract for passive planning/FormulaPlane analysis.
192    ///
193    /// The default is deliberately conservative: functions that do not opt in
194    /// must not receive dependency-summary optimization. Implementations should
195    /// return `Some` only for arities and argument roles they can describe
196    /// without under-approximating dependencies.
197    fn dependency_contract(&self, _arity: usize) -> Option<FunctionDependencyContract> {
198        None
199    }
200
201    /// Explicit semantic classification for this call arity.
202    ///
203    /// The public default is intentionally untrusted. The registry supplies a
204    /// sealed default only for crate-owned builtin registration.
205    fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
206        None
207    }
208
209    #[inline]
210    fn function_salt(&self) -> u64 {
211        // Stable hash of function name + namespace
212        let full_name = if self.namespace().is_empty() {
213            self.name().to_string()
214        } else {
215            format!("{}::{}", self.namespace(), self.name())
216        };
217        crate::rng::fnv1a64(full_name.as_bytes())
218    }
219
220    /// Derive an eval-internal scalar format annotation for this call.
221    /// Functions drop annotations by default; selection and temporal constructors override it.
222    fn propagate_format(
223        &self,
224        result: &crate::traits::CalcValue<'_>,
225    ) -> Option<crate::format::FormatId> {
226        let _ = result;
227        None
228    }
229
230    fn apply_format_propagation<'a>(
231        &self,
232        result: crate::traits::CalcValue<'a>,
233    ) -> crate::traits::CalcValue<'a> {
234        let format = self.propagate_format(&result);
235        result.with_format(format)
236    }
237
238    /// The unified evaluation path.
239    ///
240    /// This method replaces the separate scalar, fold, and map paths.
241    /// Functions use the provided `ArgumentHandle`s to access inputs as either
242    /// scalars or `RangeView`s (Arrow-backed virtual ranges).
243    fn eval<'a, 'b, 'c>(
244        &self,
245        args: &'c [ArgumentHandle<'a, 'b>],
246        ctx: &dyn crate::traits::FunctionContext<'b>,
247    ) -> Result<crate::traits::CalcValue<'b>, ExcelError>;
248
249    /// Optional reference result path. Only called by the interpreter/engine
250    /// when the callsite expects a reference (e.g., range combinators, by-ref
251    /// argument positions, or spill sources).
252    ///
253    /// Default implementation returns `None`, indicating the function does not
254    /// support returning references. Functions that set `RETURNS_REFERENCE`
255    /// should override this.
256    fn eval_reference<'a, 'b, 'c>(
257        &self,
258        _args: &'c [ArgumentHandle<'a, 'b>],
259        _ctx: &dyn crate::traits::FunctionContext<'b>,
260    ) -> Option<Result<formualizer_parse::parser::ReferenceType, ExcelError>> {
261        None
262    }
263
264    /// Resolve a reference-capable function while preserving scalar-selector caching.
265    ///
266    /// The fallback deliberately re-enters the caller's ordinary value path. That
267    /// preserves dispatch validation and array lifting for existing functions whose
268    /// `eval_reference` declines a particular argument shape. Array-valued selectors
269    /// may be evaluated once by each path.
270    fn resolve_reference_or_value<'a, 'b, 'c>(
271        &self,
272        args: &'c [ArgumentHandle<'a, 'b>],
273        ctx: &dyn crate::traits::FunctionContext<'b>,
274        value_fallback: &dyn Fn() -> Result<crate::traits::CalcValue<'b>, ExcelError>,
275    ) -> Result<FunctionResolution<'b>, ExcelError> {
276        match self.eval_reference(args, ctx) {
277            Some(Ok(reference)) => Ok(FunctionResolution::Reference(reference)),
278            Some(Err(error)) => Ok(FunctionResolution::ReferenceError(error)),
279            None => value_fallback().map(FunctionResolution::Value),
280        }
281    }
282
283    /// Dispatch to the unified evaluation path with automatic argument validation.
284    fn dispatch<'a, 'b, 'c>(
285        &self,
286        args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
287        ctx: &dyn crate::traits::FunctionContext<'b>,
288    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
289        // Short-circuit functions (IF/IFS/CHOOSE/SWITCH/AND/OR, ...) evaluate
290        // their arguments lazily inside `eval`; eagerly materializing every
291        // argument here would execute reads in untaken branches (defeating the
292        // documented short-circuit semantics) and double-evaluate taken ones.
293        // Their schemas are Any-kind with no per-arg coercion, so per-argument
294        // validation cannot fail; only the min-arity check is meaningful.
295        // (LET/LAMBDA already bypass validation via `dispatch` overrides for
296        // the same reason.)
297        if self.caps().contains(FnCaps::SHORT_CIRCUIT) {
298            if args.len() < self.min_args() {
299                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
300                    ExcelError::new(formualizer_common::ExcelErrorKind::Value).with_message(
301                        format!(
302                            "Too few arguments: expected at least {}, got {}",
303                            self.min_args(),
304                            args.len()
305                        ),
306                    ),
307                )));
308            }
309            return self
310                .eval(args, ctx)
311                .map(|result| self.apply_format_propagation(result));
312        }
313
314        // Central argument validation (includes min-arity check)
315        {
316            use crate::args::{ValidationOptions, validate_and_prepare};
317            let schema = self.arg_schema();
318            if let Err(e) = validate_and_prepare(
319                args,
320                schema,
321                ValidationOptions {
322                    warn_only: false,
323                    min_args: self.min_args(),
324                },
325            ) {
326                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
327            }
328        }
329
330        self.eval(args, ctx)
331            .map(|result| self.apply_format_propagation(result))
332    }
333}