Skip to main content

formualizer_eval/
args.rs

1use crate::traits::ArgumentHandle;
2// Note: Validator no longer depends on EvaluationContext; keep it engine-agnostic.
3use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
4use smallvec::{SmallVec, smallvec};
5use std::borrow::Cow;
6
7#[derive(Copy, Clone, Debug, Eq, PartialEq)]
8pub enum ShapeKind {
9    Scalar,
10    Range,
11    Array,
12}
13
14pub use formualizer_common::CoercionPolicy;
15
16#[derive(Clone, Debug)]
17pub struct ArgSchema {
18    pub kinds: SmallVec<[ArgKind; 2]>,
19    pub required: bool,
20    pub by_ref: bool,
21    pub shape: ShapeKind,
22    pub coercion: CoercionPolicy,
23    pub max: Option<usize>,
24    pub repeating: Option<usize>,
25    pub default: Option<LiteralValue>,
26}
27
28impl ArgSchema {
29    pub fn any() -> Self {
30        Self {
31            kinds: smallvec![ArgKind::Any],
32            required: true,
33            by_ref: false,
34            shape: ShapeKind::Scalar,
35            coercion: CoercionPolicy::None,
36            max: None,
37            repeating: None,
38            default: None,
39        }
40    }
41
42    pub fn number_lenient_scalar() -> Self {
43        Self {
44            kinds: smallvec![ArgKind::Number],
45            required: true,
46            by_ref: false,
47            shape: ShapeKind::Scalar,
48            coercion: CoercionPolicy::NumberLenientText,
49            max: None,
50            repeating: None,
51            default: None,
52        }
53    }
54}
55
56#[derive(Clone, Debug)]
57pub enum CriteriaPredicate {
58    Eq(LiteralValue),
59    Ne(LiteralValue),
60    Gt(f64),
61    Ge(f64),
62    Lt(f64),
63    Le(f64),
64    TextLike {
65        pattern: String,
66        case_insensitive: bool,
67    },
68    IsBlank,
69    IsNumber,
70    IsText,
71    IsLogical,
72}
73
74#[derive(Debug)]
75pub enum PreparedArg<'a> {
76    Value(Cow<'a, LiteralValue>),
77    Range(crate::engine::range_view::RangeView<'a>),
78    Reference(formualizer_parse::parser::ReferenceType),
79    Predicate(CriteriaPredicate),
80}
81
82pub struct PreparedArgs<'a> {
83    pub items: Vec<PreparedArg<'a>>,
84}
85
86#[derive(Default)]
87pub struct ValidationOptions {
88    pub warn_only: bool,
89    /// Minimum number of arguments the function requires.  When non-zero,
90    /// `validate_and_prepare` rejects calls with fewer arguments before any
91    /// per-argument validation runs, preventing out-of-bounds panics in
92    /// `eval` implementations.
93    pub min_args: usize,
94}
95
96// Legacy adapter removed in clean break.
97
98pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
99    match v {
100        LiteralValue::Text(s) => {
101            let s_trim = s.trim();
102
103            let unquote = |t: &str| -> String {
104                let t = t.trim();
105                if let Some(inner) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
106                    inner.replace("\"\"", "\"")
107                } else {
108                    t.to_string()
109                }
110            };
111
112            // Operators: >=, <=, <>, >, <, =
113            let ops = [">=", "<=", "<>", ">", "<", "="];
114            for op in ops.iter() {
115                if let Some(rhs) = s_trim.strip_prefix(op) {
116                    let rhs_trim = rhs.trim();
117                    // Try numeric parse for comparisons
118                    if let Ok(n) = rhs_trim.parse::<f64>() {
119                        return Ok(match *op {
120                            ">=" => CriteriaPredicate::Ge(n),
121                            "<=" => CriteriaPredicate::Le(n),
122                            ">" => CriteriaPredicate::Gt(n),
123                            "<" => CriteriaPredicate::Lt(n),
124                            "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
125                            "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
126                            _ => unreachable!(),
127                        });
128                    }
129                    // Fallback: non-numeric equals/neq text (support Excel-style quoted strings: ="aa")
130                    let lit = LiteralValue::Text(unquote(rhs_trim));
131                    return Ok(match *op {
132                        "=" => CriteriaPredicate::Eq(lit),
133                        "<>" => CriteriaPredicate::Ne(lit),
134                        ">=" | "<=" | ">" | "<" => {
135                            // Non-numeric compare: not fully supported; degrade to equality on full expression
136                            CriteriaPredicate::Eq(LiteralValue::Text(s_trim.to_string()))
137                        }
138                        _ => unreachable!(),
139                    });
140                }
141            }
142
143            let plain = unquote(s_trim);
144
145            // Wildcards * or ? => TextLike
146            if plain.contains('*') || plain.contains('?') {
147                return Ok(CriteriaPredicate::TextLike {
148                    pattern: plain,
149                    case_insensitive: true,
150                });
151            }
152            // Booleans TRUE/FALSE
153            let lower = plain.to_ascii_lowercase();
154            if lower == "true" {
155                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
156            } else if lower == "false" {
157                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
158            }
159            // Plain text equality
160            Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
161        }
162        LiteralValue::Empty => Ok(CriteriaPredicate::IsBlank),
163        LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
164        // Normalize integer criteria to Number for Excel-style numeric coercions
165        // (e.g. blank == 0, numeric text == number, etc.)
166        LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
167        LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
168        LiteralValue::Error(e) => Err(e.clone()),
169        LiteralValue::Array(arr) => {
170            // Treat 1x1 array literals as scalars for criteria parsing
171            if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
172                parse_criteria(&arr[0][0])
173            } else {
174                Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
175            }
176        }
177        other => Ok(CriteriaPredicate::Eq(other.clone())),
178    }
179}
180
181pub fn validate_and_prepare<'a, 'b>(
182    args: &'a [ArgumentHandle<'a, 'b>],
183    schema: &[ArgSchema],
184    options: ValidationOptions,
185) -> Result<PreparedArgs<'a>, ExcelError> {
186    // Minimum arity — reject too-few arguments before per-arg validation so
187    // that individual `eval` implementations cannot panic on indexing.
188    if options.min_args > 0 && args.len() < options.min_args {
189        if options.warn_only {
190            return Ok(PreparedArgs { items: Vec::new() });
191        }
192        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
193            "Too few arguments: expected at least {}, got {}",
194            options.min_args,
195            args.len()
196        )));
197    }
198
199    // Arity: simple rule – if schema.len() == 1, allow variadic repetition; else match up to schema.len()
200    if schema.is_empty() {
201        return Ok(PreparedArgs { items: Vec::new() });
202    }
203
204    let mut items: Vec<PreparedArg<'a>> = Vec::with_capacity(args.len());
205    for (idx, arg) in args.iter().enumerate() {
206        let spec = if schema.len() == 1 {
207            &schema[0]
208        } else if idx < schema.len() {
209            &schema[idx]
210        } else {
211            // Attempt to find a repeating spec (e.g., variadic tail like CHOOSE, SUM, etc.)
212            if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
213                rep_spec
214            } else if options.warn_only {
215                continue;
216            } else {
217                return Err(
218                    ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
219                );
220            }
221        };
222
223        // By-ref argument: require a reference (AST literal or function-returned)
224        if spec.by_ref {
225            match arg.as_reference_or_eval() {
226                Ok(r) => {
227                    items.push(PreparedArg::Reference(r));
228                    continue;
229                }
230                Err(e) => {
231                    if options.warn_only {
232                        continue;
233                    } else {
234                        return Err(e);
235                    }
236                }
237            }
238        }
239
240        // Criteria policy: parse into predicate
241        if matches!(spec.coercion, CoercionPolicy::Criteria) {
242            let v = arg.value()?.into_literal();
243            match parse_criteria(&v) {
244                Ok(pred) => {
245                    items.push(PreparedArg::Predicate(pred));
246                    continue;
247                }
248                Err(e) => {
249                    if options.warn_only {
250                        continue;
251                    } else {
252                        return Err(e);
253                    }
254                }
255            }
256        }
257
258        // Shape handling
259        match spec.shape {
260            ShapeKind::Scalar => {
261                // Collapse to scalar if needed (top-left for arrays)
262                match arg.value() {
263                    Ok(cv) => {
264                        let v: Cow<'_, LiteralValue> = match cv {
265                            crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
266                                let tl = arr
267                                    .first()
268                                    .and_then(|row| row.first())
269                                    .cloned()
270                                    .unwrap_or(LiteralValue::Empty);
271                                Cow::Owned(tl)
272                            }
273                            crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
274                            crate::traits::CalcValue::Scalar(s) => Cow::Owned(s),
275                            crate::traits::CalcValue::Callable(_) => {
276                                Cow::Owned(LiteralValue::Error(
277                                    ExcelError::new(ExcelErrorKind::Calc)
278                                        .with_message("LAMBDA value must be invoked"),
279                                ))
280                            }
281                        };
282                        // Apply coercion policy to Value shapes when applicable
283                        let coerced = match spec.coercion {
284                            CoercionPolicy::None => v,
285                            CoercionPolicy::NumberStrict => {
286                                match crate::coercion::to_number_strict(v.as_ref()) {
287                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
288                                    Err(e) => {
289                                        if options.warn_only {
290                                            v
291                                        } else {
292                                            return Err(e);
293                                        }
294                                    }
295                                }
296                            }
297                            CoercionPolicy::NumberLenientText => {
298                                match crate::coercion::to_number_lenient(v.as_ref()) {
299                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
300                                    Err(e) => {
301                                        if options.warn_only {
302                                            v
303                                        } else {
304                                            return Err(e);
305                                        }
306                                    }
307                                }
308                            }
309                            CoercionPolicy::Logical => {
310                                match crate::coercion::to_logical(v.as_ref()) {
311                                    Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
312                                    Err(e) => {
313                                        if options.warn_only {
314                                            v
315                                        } else {
316                                            return Err(e);
317                                        }
318                                    }
319                                }
320                            }
321                            CoercionPolicy::Criteria => v, // handled per-function currently
322                            CoercionPolicy::DateTimeSerial => {
323                                match crate::coercion::to_datetime_serial(v.as_ref()) {
324                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
325                                    Err(e) => {
326                                        if options.warn_only {
327                                            v
328                                        } else {
329                                            return Err(e);
330                                        }
331                                    }
332                                }
333                            }
334                        };
335                        items.push(PreparedArg::Value(coerced))
336                    }
337                    Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
338                }
339            }
340            ShapeKind::Range | ShapeKind::Array => {
341                match arg.range_view() {
342                    Ok(r) => items.push(PreparedArg::Range(r)),
343                    Err(_e) => {
344                        // Excel-compatible: functions that accept ranges typically also accept scalars.
345                        // Fall back to treating the argument as a scalar value, even in strict mode.
346                        match arg.value() {
347                            Ok(v) => items.push(PreparedArg::Value(Cow::Owned(v.into_literal()))),
348                            Err(e2) => {
349                                items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e2))))
350                            }
351                        }
352                    }
353                }
354            }
355        }
356    }
357
358    Ok(PreparedArgs { items })
359}