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).
108pub trait Function: Send + Sync + 'static {
109 /// Capability flags for this function
110 fn caps(&self) -> FnCaps {
111 FnCaps::PURE
112 }
113
114 fn name(&self) -> &'static str;
115 fn namespace(&self) -> &'static str {
116 ""
117 }
118 fn min_args(&self) -> usize {
119 0
120 }
121 fn variadic(&self) -> bool {
122 false
123 }
124 fn volatile(&self) -> bool {
125 self.caps().contains(FnCaps::VOLATILE)
126 }
127 fn arg_schema(&self) -> &'static [ArgSchema] {
128 if self.min_args() > 0 {
129 panic!("Non-zero min_args must have a valid arg_schema");
130 } else {
131 &[]
132 }
133 }
134
135 /// Optional list of additional alias names (case-insensitive) that should resolve to this
136 /// function. Default: empty slice. Implementors can override to expose legacy names.
137 /// Returned slice must have 'static lifetime (typically a static array reference).
138 fn aliases(&self) -> &'static [&'static str] {
139 &[]
140 }
141
142 /// Optional dependency contract for passive planning/FormulaPlane analysis.
143 ///
144 /// The default is deliberately conservative: functions that do not opt in
145 /// must not receive dependency-summary optimization. Implementations should
146 /// return `Some` only for arities and argument roles they can describe
147 /// without under-approximating dependencies.
148 fn dependency_contract(&self, _arity: usize) -> Option<FunctionDependencyContract> {
149 None
150 }
151
152 /// Explicit semantic classification for this call arity.
153 ///
154 /// The public default is intentionally untrusted. The registry supplies a
155 /// sealed default only for crate-owned builtin registration.
156 fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
157 None
158 }
159
160 #[inline]
161 fn function_salt(&self) -> u64 {
162 // Stable hash of function name + namespace
163 let full_name = if self.namespace().is_empty() {
164 self.name().to_string()
165 } else {
166 format!("{}::{}", self.namespace(), self.name())
167 };
168 crate::rng::fnv1a64(full_name.as_bytes())
169 }
170
171 /// Derive an eval-internal scalar format annotation for this call.
172 /// Functions drop annotations by default; selection and temporal constructors override it.
173 fn propagate_format(
174 &self,
175 result: &crate::traits::CalcValue<'_>,
176 ) -> Option<crate::format::FormatId> {
177 let _ = result;
178 None
179 }
180
181 fn apply_format_propagation<'a>(
182 &self,
183 result: crate::traits::CalcValue<'a>,
184 ) -> crate::traits::CalcValue<'a> {
185 let format = self.propagate_format(&result);
186 result.with_format(format)
187 }
188
189 /// The unified evaluation path.
190 ///
191 /// This method replaces the separate scalar, fold, and map paths.
192 /// Functions use the provided `ArgumentHandle`s to access inputs as either
193 /// scalars or `RangeView`s (Arrow-backed virtual ranges).
194 fn eval<'a, 'b, 'c>(
195 &self,
196 args: &'c [ArgumentHandle<'a, 'b>],
197 ctx: &dyn crate::traits::FunctionContext<'b>,
198 ) -> Result<crate::traits::CalcValue<'b>, ExcelError>;
199
200 /// Optional reference result path. Only called by the interpreter/engine
201 /// when the callsite expects a reference (e.g., range combinators, by-ref
202 /// argument positions, or spill sources).
203 ///
204 /// Default implementation returns `None`, indicating the function does not
205 /// support returning references. Functions that set `RETURNS_REFERENCE`
206 /// should override this.
207 fn eval_reference<'a, 'b, 'c>(
208 &self,
209 _args: &'c [ArgumentHandle<'a, 'b>],
210 _ctx: &dyn crate::traits::FunctionContext<'b>,
211 ) -> Option<Result<formualizer_parse::parser::ReferenceType, ExcelError>> {
212 None
213 }
214
215 /// Resolve a reference-capable function while preserving scalar-selector caching.
216 ///
217 /// The fallback deliberately re-enters the caller's ordinary value path. That
218 /// preserves dispatch validation and array lifting for existing functions whose
219 /// `eval_reference` declines a particular argument shape. Array-valued selectors
220 /// may be evaluated once by each path.
221 fn resolve_reference_or_value<'a, 'b, 'c>(
222 &self,
223 args: &'c [ArgumentHandle<'a, 'b>],
224 ctx: &dyn crate::traits::FunctionContext<'b>,
225 value_fallback: &dyn Fn() -> Result<crate::traits::CalcValue<'b>, ExcelError>,
226 ) -> Result<FunctionResolution<'b>, ExcelError> {
227 match self.eval_reference(args, ctx) {
228 Some(Ok(reference)) => Ok(FunctionResolution::Reference(reference)),
229 Some(Err(error)) => Ok(FunctionResolution::ReferenceError(error)),
230 None => value_fallback().map(FunctionResolution::Value),
231 }
232 }
233
234 /// Dispatch to the unified evaluation path with automatic argument validation.
235 fn dispatch<'a, 'b, 'c>(
236 &self,
237 args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
238 ctx: &dyn crate::traits::FunctionContext<'b>,
239 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
240 // Short-circuit functions (IF/IFS/CHOOSE/SWITCH/AND/OR, ...) evaluate
241 // their arguments lazily inside `eval`; eagerly materializing every
242 // argument here would execute reads in untaken branches (defeating the
243 // documented short-circuit semantics) and double-evaluate taken ones.
244 // Their schemas are Any-kind with no per-arg coercion, so per-argument
245 // validation cannot fail; only the min-arity check is meaningful.
246 // (LET/LAMBDA already bypass validation via `dispatch` overrides for
247 // the same reason.)
248 if self.caps().contains(FnCaps::SHORT_CIRCUIT) {
249 if args.len() < self.min_args() {
250 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
251 ExcelError::new(formualizer_common::ExcelErrorKind::Value).with_message(
252 format!(
253 "Too few arguments: expected at least {}, got {}",
254 self.min_args(),
255 args.len()
256 ),
257 ),
258 )));
259 }
260 return self
261 .eval(args, ctx)
262 .map(|result| self.apply_format_propagation(result));
263 }
264
265 // Central argument validation (includes min-arity check)
266 {
267 use crate::args::{ValidationOptions, validate_and_prepare};
268 let schema = self.arg_schema();
269 if let Err(e) = validate_and_prepare(
270 args,
271 schema,
272 ValidationOptions {
273 warn_only: false,
274 min_args: self.min_args(),
275 },
276 ) {
277 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
278 }
279 }
280
281 self.eval(args, ctx)
282 .map(|result| self.apply_format_propagation(result))
283 }
284}