1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum FunctionDependencyClass {
10 StaticScalarAllArgs,
12 StaticReduction,
14 CriteriaAggregation,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum FunctionArgumentDependencyRole {
21 ScalarValue,
22 FiniteRangeValue,
23 ReductionValue,
24 CriteriaRange,
25 CriteriaExpression,
26 ValueRange,
27 LazyBranch,
28 LookupKey,
29 LookupTable,
30 LookupResultSelector,
31 ByReference,
32 LocalBindingName,
33 LocalBindingValue,
34 LambdaBody,
35 IgnoredLiteral,
36 Unsupported,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum FunctionArityRule {
41 Exactly(usize),
42 AtLeast(usize),
43 OneOf(&'static [usize]),
44 EvenAtLeast(usize),
45 OddAtLeast(usize),
46}
47
48impl FunctionArityRule {
49 pub fn allows(self, arity: usize) -> bool {
50 match self {
51 Self::Exactly(expected) => arity == expected,
52 Self::AtLeast(min) => arity >= min,
53 Self::OneOf(allowed) => allowed.contains(&arity),
54 Self::EvenAtLeast(min) => arity >= min && arity.is_multiple_of(2),
55 Self::OddAtLeast(min) => arity >= min && !arity.is_multiple_of(2),
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
61pub enum CriteriaValueRange {
62 None,
65 Fixed(usize),
67 Optional {
70 provided_index: usize,
71 fallback_criteria_range_index: usize,
72 },
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76pub struct CriteriaAggregationDependencyContract {
77 pub value_range: CriteriaValueRange,
78 pub first_criteria_pair: usize,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub enum FunctionArgumentDependencyContract {
83 AllArgs(FunctionArgumentDependencyRole),
84 Variadic(FunctionArgumentDependencyRole),
85 CriteriaPairs(CriteriaAggregationDependencyContract),
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89pub struct FunctionDependencyContract {
90 pub class: FunctionDependencyClass,
91 pub arity: FunctionArityRule,
92 pub arguments: FunctionArgumentDependencyContract,
93}
94
95impl FunctionDependencyContract {
96 pub fn static_scalar_all_args(arity: usize) -> Option<Self> {
97 Self {
98 class: FunctionDependencyClass::StaticScalarAllArgs,
99 arity: FunctionArityRule::Exactly(1),
100 arguments: FunctionArgumentDependencyContract::AllArgs(
101 FunctionArgumentDependencyRole::ScalarValue,
102 ),
103 }
104 .for_arity(arity)
105 }
106
107 pub fn static_reduction(arity: usize, min_args: usize) -> Option<Self> {
108 Self {
109 class: FunctionDependencyClass::StaticReduction,
110 arity: FunctionArityRule::AtLeast(min_args),
111 arguments: FunctionArgumentDependencyContract::Variadic(
112 FunctionArgumentDependencyRole::ReductionValue,
113 ),
114 }
115 .for_arity(arity)
116 }
117
118 pub fn criteria_aggregation(
119 arity: usize,
120 arity_rule: FunctionArityRule,
121 value_range: CriteriaValueRange,
122 first_criteria_pair: usize,
123 ) -> Option<Self> {
124 Self {
125 class: FunctionDependencyClass::CriteriaAggregation,
126 arity: arity_rule,
127 arguments: FunctionArgumentDependencyContract::CriteriaPairs(
128 CriteriaAggregationDependencyContract {
129 value_range,
130 first_criteria_pair,
131 },
132 ),
133 }
134 .for_arity(arity)
135 }
136
137 pub fn for_arity(self, arity: usize) -> Option<Self> {
138 self.arity.allows(arity).then_some(self)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::function::Function;
146 use crate::traits::{ArgumentHandle, FunctionContext};
147 use formualizer_common::ExcelError;
148
149 struct NoOptInFn;
150
151 impl Function for NoOptInFn {
152 fn name(&self) -> &'static str {
153 "NO_OPT_IN"
154 }
155
156 fn eval<'a, 'b, 'c>(
157 &self,
158 _args: &'c [ArgumentHandle<'a, 'b>],
159 _ctx: &dyn FunctionContext<'b>,
160 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
161 unreachable!("contract tests never evaluate")
162 }
163 }
164
165 #[test]
166 fn default_function_dependency_contract_is_conservative_none() {
167 let function = NoOptInFn;
168
169 assert_eq!(function.dependency_contract(0), None);
170 assert_eq!(function.dependency_contract(1), None);
171 assert_eq!(function.dependency_contract(3), None);
172 }
173
174 #[test]
175 fn arity_rules_are_explicit_and_bounded() {
176 assert!(FunctionArityRule::Exactly(1).allows(1));
177 assert!(!FunctionArityRule::Exactly(1).allows(0));
178 assert!(FunctionArityRule::AtLeast(0).allows(0));
179 assert!(FunctionArityRule::AtLeast(1).allows(3));
180 assert!(!FunctionArityRule::AtLeast(2).allows(1));
181 assert!(FunctionArityRule::OneOf(&[2, 3]).allows(3));
182 assert!(!FunctionArityRule::OneOf(&[2, 3]).allows(4));
183 assert!(FunctionArityRule::EvenAtLeast(2).allows(4));
184 assert!(!FunctionArityRule::EvenAtLeast(2).allows(3));
185 assert!(FunctionArityRule::OddAtLeast(3).allows(5));
186 assert!(!FunctionArityRule::OddAtLeast(3).allows(4));
187 }
188
189 #[test]
190 fn constructors_return_none_for_unsupported_arities() {
191 assert!(FunctionDependencyContract::static_scalar_all_args(1).is_some());
192 assert_eq!(FunctionDependencyContract::static_scalar_all_args(2), None);
193
194 assert!(FunctionDependencyContract::static_reduction(0, 0).is_some());
195 assert_eq!(FunctionDependencyContract::static_reduction(0, 1), None);
196
197 assert!(
198 FunctionDependencyContract::criteria_aggregation(
199 4,
200 FunctionArityRule::EvenAtLeast(2),
201 CriteriaValueRange::None,
202 0,
203 )
204 .is_some()
205 );
206 assert_eq!(
207 FunctionDependencyContract::criteria_aggregation(
208 3,
209 FunctionArityRule::EvenAtLeast(2),
210 CriteriaValueRange::None,
211 0,
212 ),
213 None
214 );
215 }
216
217 #[test]
218 fn selected_builtin_opt_ins_are_colocated_and_arity_gated() {
219 use crate::builtins::math::aggregate::{AverageFn, SumFn};
220 use crate::builtins::math::criteria_aggregates::{CountIfsFn, SumIfFn, SumIfsFn};
221 use crate::builtins::math::numeric::AbsFn;
222
223 let abs = AbsFn;
224 assert_eq!(
225 abs.dependency_contract(1).map(|contract| contract.class),
226 Some(FunctionDependencyClass::StaticScalarAllArgs)
227 );
228 assert_eq!(abs.dependency_contract(2), None);
229
230 let sum = SumFn;
231 assert_eq!(
232 sum.dependency_contract(0).map(|contract| contract.class),
233 Some(FunctionDependencyClass::StaticReduction)
234 );
235
236 let average = AverageFn;
237 assert_eq!(average.dependency_contract(0), None);
238 assert_eq!(
239 average
240 .dependency_contract(1)
241 .map(|contract| contract.class),
242 Some(FunctionDependencyClass::StaticReduction)
243 );
244
245 let countifs = CountIfsFn;
246 assert!(countifs.dependency_contract(2).is_some());
247 assert!(countifs.dependency_contract(4).is_some());
248 assert_eq!(countifs.dependency_contract(3), None);
249
250 let sumif = SumIfFn;
251 let contract = sumif.dependency_contract(3).expect("SUMIF arity 3");
252 assert_eq!(contract.class, FunctionDependencyClass::CriteriaAggregation);
253 assert_eq!(
254 contract.arguments,
255 FunctionArgumentDependencyContract::CriteriaPairs(
256 CriteriaAggregationDependencyContract {
257 value_range: CriteriaValueRange::Optional {
258 provided_index: 2,
259 fallback_criteria_range_index: 0,
260 },
261 first_criteria_pair: 0,
262 }
263 )
264 );
265
266 let sumifs = SumIfsFn;
267 assert!(sumifs.dependency_contract(3).is_some());
268 assert!(sumifs.dependency_contract(5).is_some());
269 assert_eq!(sumifs.dependency_contract(4), None);
270 }
271}