1use crate::function::FnCaps;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum FunctionDependencySemantics {
10 RecursiveSyntacticArgs,
11 Dynamic,
12 Unsupported,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum FunctionEvaluationSemantics {
17 Eager,
18 ShortCircuit,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum FunctionResultSemantics {
23 ScalarValue,
24 MayReturnReference,
25 MaySpill,
26 MayReturnReferenceAndSpill,
27 Unknown,
28}
29
30impl FunctionResultSemantics {
31 pub fn from_capabilities(may_return_reference: bool, may_spill: bool) -> Self {
32 match (may_return_reference, may_spill) {
33 (false, false) => Self::ScalarValue,
34 (true, false) => Self::MayReturnReference,
35 (false, true) => Self::MaySpill,
36 (true, true) => Self::MayReturnReferenceAndSpill,
37 }
38 }
39
40 pub fn may_return_reference(self) -> bool {
41 matches!(
42 self,
43 Self::MayReturnReference | Self::MayReturnReferenceAndSpill
44 )
45 }
46
47 pub fn may_spill(self) -> bool {
48 matches!(self, Self::MaySpill | Self::MayReturnReferenceAndSpill)
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub enum FunctionEnvironmentSemantics {
54 None,
55 LocalBindings,
56 Unknown,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub enum FunctionContextDependence {
61 None,
62 PlacementDependent,
63 WorkbookMetadata,
64 LocaleOrConfiguration,
65 Unsupported,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub struct FunctionSemanticContract {
70 pub dependency: FunctionDependencySemantics,
71 pub evaluation: FunctionEvaluationSemantics,
72 pub result: FunctionResultSemantics,
73 pub environment: FunctionEnvironmentSemantics,
74 pub context: FunctionContextDependence,
75 pub precision: Option<FunctionDependencyContract>,
76}
77
78impl FunctionSemanticContract {
79 pub fn trusted_builtin_default(precision: Option<FunctionDependencyContract>) -> Self {
80 Self {
81 dependency: FunctionDependencySemantics::RecursiveSyntacticArgs,
82 evaluation: FunctionEvaluationSemantics::Eager,
83 result: FunctionResultSemantics::ScalarValue,
84 environment: FunctionEnvironmentSemantics::None,
85 context: FunctionContextDependence::None,
86 precision,
87 }
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Hash)]
92pub struct FunctionSemanticIdentity {
93 pub(crate) namespace: String,
94 pub(crate) canonical_name: String,
95 pub(crate) generation: u64,
96 pub(crate) caps: FnCaps,
97 pub(crate) contract: FunctionSemanticContract,
98 pub(crate) argument_by_ref: Vec<bool>,
99}
100
101impl FunctionSemanticIdentity {
102 pub(crate) fn encode(&self) -> Vec<u8> {
103 let mut out = Vec::new();
104 push_bytes(&mut out, self.namespace.as_bytes());
105 push_bytes(&mut out, self.canonical_name.as_bytes());
106 out.extend_from_slice(&self.generation.to_le_bytes());
107 out.extend_from_slice(&self.caps.bits().to_le_bytes());
108 out.push(self.contract.dependency as u8);
109 out.push(self.contract.evaluation as u8);
110 out.push(self.contract.result as u8);
111 out.push(self.contract.environment as u8);
112 out.push(self.contract.context as u8);
113 match self.contract.precision {
114 Some(precision) => {
115 out.push(1);
116 encode_precision(&mut out, precision);
117 }
118 None => out.push(0),
119 }
120 out.extend_from_slice(&(self.argument_by_ref.len() as u64).to_le_bytes());
121 out.extend(self.argument_by_ref.iter().map(|value| u8::from(*value)));
122 out
123 }
124}
125
126fn push_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
127 out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
128 out.extend_from_slice(bytes);
129}
130
131fn encode_precision(out: &mut Vec<u8>, precision: FunctionDependencyContract) {
132 out.push(precision.class as u8);
133 match precision.arity {
134 FunctionArityRule::Exactly(value) => encode_arity(out, 0, value),
135 FunctionArityRule::AtLeast(value) => encode_arity(out, 1, value),
136 FunctionArityRule::OneOf(values) => {
137 out.push(2);
138 out.extend_from_slice(&(values.len() as u64).to_le_bytes());
139 for value in values {
140 out.extend_from_slice(&(*value as u64).to_le_bytes());
141 }
142 }
143 FunctionArityRule::EvenAtLeast(value) => encode_arity(out, 3, value),
144 FunctionArityRule::OddAtLeast(value) => encode_arity(out, 4, value),
145 }
146 match precision.arguments {
147 FunctionArgumentDependencyContract::AllArgs(role) => encode_role(out, 0, role),
148 FunctionArgumentDependencyContract::Variadic(role) => encode_role(out, 1, role),
149 FunctionArgumentDependencyContract::CriteriaPairs(criteria) => {
150 out.push(2);
151 match criteria.value_range {
152 CriteriaValueRange::None => out.push(0),
153 CriteriaValueRange::Fixed(index) => encode_arity(out, 1, index),
154 CriteriaValueRange::Optional {
155 provided_index,
156 fallback_criteria_range_index,
157 } => {
158 encode_arity(out, 2, provided_index);
159 out.extend_from_slice(&(fallback_criteria_range_index as u64).to_le_bytes());
160 }
161 }
162 out.extend_from_slice(&(criteria.first_criteria_pair as u64).to_le_bytes());
163 }
164 FunctionArgumentDependencyContract::LocalBindingPairs => out.push(3),
165 FunctionArgumentDependencyContract::LambdaParameters => out.push(4),
166 }
167}
168
169fn encode_arity(out: &mut Vec<u8>, tag: u8, value: usize) {
170 out.push(tag);
171 out.extend_from_slice(&(value as u64).to_le_bytes());
172}
173
174fn encode_role(out: &mut Vec<u8>, tag: u8, role: FunctionArgumentDependencyRole) {
175 out.push(tag);
176 out.push(role as u8);
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
180pub enum FunctionDependencyClass {
181 StaticScalarAllArgs,
183 StaticReduction,
185 CriteriaAggregation,
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
191pub enum FunctionArgumentDependencyRole {
192 ScalarValue,
193 FiniteRangeValue,
194 ReductionValue,
195 CriteriaRange,
196 CriteriaExpression,
197 ValueRange,
198 LazyBranch,
199 LookupKey,
200 LookupTable,
201 LookupResultSelector,
202 ByReference,
203 LocalBindingName,
204 LocalBindingValue,
205 LambdaBody,
206 IgnoredLiteral,
207 Unsupported,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
211pub enum FunctionArityRule {
212 Exactly(usize),
213 AtLeast(usize),
214 OneOf(&'static [usize]),
215 EvenAtLeast(usize),
216 OddAtLeast(usize),
217}
218
219impl FunctionArityRule {
220 pub fn allows(self, arity: usize) -> bool {
221 match self {
222 Self::Exactly(expected) => arity == expected,
223 Self::AtLeast(min) => arity >= min,
224 Self::OneOf(allowed) => allowed.contains(&arity),
225 Self::EvenAtLeast(min) => arity >= min && arity.is_multiple_of(2),
226 Self::OddAtLeast(min) => arity >= min && !arity.is_multiple_of(2),
227 }
228 }
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
232pub enum CriteriaValueRange {
233 None,
236 Fixed(usize),
238 Optional {
241 provided_index: usize,
242 fallback_criteria_range_index: usize,
243 },
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
247pub struct CriteriaAggregationDependencyContract {
248 pub value_range: CriteriaValueRange,
249 pub first_criteria_pair: usize,
250}
251
252#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
253pub enum FunctionArgumentDependencyContract {
254 AllArgs(FunctionArgumentDependencyRole),
255 Variadic(FunctionArgumentDependencyRole),
256 CriteriaPairs(CriteriaAggregationDependencyContract),
257 LocalBindingPairs,
258 LambdaParameters,
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
262pub struct FunctionDependencyContract {
263 pub class: FunctionDependencyClass,
264 pub arity: FunctionArityRule,
265 pub arguments: FunctionArgumentDependencyContract,
266}
267
268impl FunctionDependencyContract {
269 pub fn static_scalar_all_args(arity: usize) -> Option<Self> {
270 Self {
271 class: FunctionDependencyClass::StaticScalarAllArgs,
272 arity: FunctionArityRule::Exactly(1),
273 arguments: FunctionArgumentDependencyContract::AllArgs(
274 FunctionArgumentDependencyRole::ScalarValue,
275 ),
276 }
277 .for_arity(arity)
278 }
279
280 pub fn static_reduction(arity: usize, min_args: usize) -> Option<Self> {
281 Self {
282 class: FunctionDependencyClass::StaticReduction,
283 arity: FunctionArityRule::AtLeast(min_args),
284 arguments: FunctionArgumentDependencyContract::Variadic(
285 FunctionArgumentDependencyRole::ReductionValue,
286 ),
287 }
288 .for_arity(arity)
289 }
290
291 pub fn criteria_aggregation(
292 arity: usize,
293 arity_rule: FunctionArityRule,
294 value_range: CriteriaValueRange,
295 first_criteria_pair: usize,
296 ) -> Option<Self> {
297 Self {
298 class: FunctionDependencyClass::CriteriaAggregation,
299 arity: arity_rule,
300 arguments: FunctionArgumentDependencyContract::CriteriaPairs(
301 CriteriaAggregationDependencyContract {
302 value_range,
303 first_criteria_pair,
304 },
305 ),
306 }
307 .for_arity(arity)
308 }
309
310 pub fn for_arity(self, arity: usize) -> Option<Self> {
311 self.arity.allows(arity).then_some(self)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::function::Function;
319 use crate::traits::{ArgumentHandle, FunctionContext};
320 use formualizer_common::ExcelError;
321
322 struct NoOptInFn;
323
324 impl Function for NoOptInFn {
325 fn name(&self) -> &'static str {
326 "NO_OPT_IN"
327 }
328
329 fn eval<'a, 'b, 'c>(
330 &self,
331 _args: &'c [ArgumentHandle<'a, 'b>],
332 _ctx: &dyn FunctionContext<'b>,
333 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
334 unreachable!("contract tests never evaluate")
335 }
336 }
337
338 #[test]
339 fn default_function_dependency_contract_is_conservative_none() {
340 let function = NoOptInFn;
341
342 assert_eq!(function.dependency_contract(0), None);
343 assert_eq!(function.dependency_contract(1), None);
344 assert_eq!(function.dependency_contract(3), None);
345 }
346
347 #[test]
348 fn arity_rules_are_explicit_and_bounded() {
349 assert!(FunctionArityRule::Exactly(1).allows(1));
350 assert!(!FunctionArityRule::Exactly(1).allows(0));
351 assert!(FunctionArityRule::AtLeast(0).allows(0));
352 assert!(FunctionArityRule::AtLeast(1).allows(3));
353 assert!(!FunctionArityRule::AtLeast(2).allows(1));
354 assert!(FunctionArityRule::OneOf(&[2, 3]).allows(3));
355 assert!(!FunctionArityRule::OneOf(&[2, 3]).allows(4));
356 assert!(FunctionArityRule::EvenAtLeast(2).allows(4));
357 assert!(!FunctionArityRule::EvenAtLeast(2).allows(3));
358 assert!(FunctionArityRule::OddAtLeast(3).allows(5));
359 assert!(!FunctionArityRule::OddAtLeast(3).allows(4));
360 }
361
362 #[test]
363 fn constructors_return_none_for_unsupported_arities() {
364 assert!(FunctionDependencyContract::static_scalar_all_args(1).is_some());
365 assert_eq!(FunctionDependencyContract::static_scalar_all_args(2), None);
366
367 assert!(FunctionDependencyContract::static_reduction(0, 0).is_some());
368 assert_eq!(FunctionDependencyContract::static_reduction(0, 1), None);
369
370 assert!(
371 FunctionDependencyContract::criteria_aggregation(
372 4,
373 FunctionArityRule::EvenAtLeast(2),
374 CriteriaValueRange::None,
375 0,
376 )
377 .is_some()
378 );
379 assert_eq!(
380 FunctionDependencyContract::criteria_aggregation(
381 3,
382 FunctionArityRule::EvenAtLeast(2),
383 CriteriaValueRange::None,
384 0,
385 ),
386 None
387 );
388 }
389
390 #[test]
391 fn selected_builtin_opt_ins_are_colocated_and_arity_gated() {
392 use crate::builtins::math::aggregate::{AverageFn, SumFn};
393 use crate::builtins::math::criteria_aggregates::{CountIfsFn, SumIfFn, SumIfsFn};
394 use crate::builtins::math::numeric::AbsFn;
395
396 let abs = AbsFn;
397 assert_eq!(
398 abs.dependency_contract(1).map(|contract| contract.class),
399 Some(FunctionDependencyClass::StaticScalarAllArgs)
400 );
401 assert_eq!(abs.dependency_contract(2), None);
402
403 let sum = SumFn;
404 assert_eq!(
405 sum.dependency_contract(0).map(|contract| contract.class),
406 Some(FunctionDependencyClass::StaticReduction)
407 );
408
409 let average = AverageFn;
410 assert_eq!(average.dependency_contract(0), None);
411 assert_eq!(
412 average
413 .dependency_contract(1)
414 .map(|contract| contract.class),
415 Some(FunctionDependencyClass::StaticReduction)
416 );
417
418 let countifs = CountIfsFn;
419 assert!(countifs.dependency_contract(2).is_some());
420 assert!(countifs.dependency_contract(4).is_some());
421 assert_eq!(countifs.dependency_contract(3), None);
422
423 let sumif = SumIfFn;
424 let contract = sumif.dependency_contract(3).expect("SUMIF arity 3");
425 assert_eq!(contract.class, FunctionDependencyClass::CriteriaAggregation);
426 assert_eq!(
427 contract.arguments,
428 FunctionArgumentDependencyContract::CriteriaPairs(
429 CriteriaAggregationDependencyContract {
430 value_range: CriteriaValueRange::Optional {
431 provided_index: 2,
432 fallback_criteria_range_index: 0,
433 },
434 first_criteria_pair: 0,
435 }
436 )
437 );
438
439 let sumifs = SumIfsFn;
440 assert!(sumifs.dependency_contract(3).is_some());
441 assert!(sumifs.dependency_contract(5).is_some());
442 assert_eq!(sumifs.dependency_contract(4), None);
443 }
444}