formualizer_eval/builtins/
logical.rs

1// crates/formualizer-eval/src/builtins/logical.rs
2
3use super::utils::ARG_ANY_ONE;
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, FunctionContext};
7use formualizer_common::{ExcelError, LiteralValue};
8use formualizer_macros::func_caps;
9
10/* ─────────────────────────── TRUE() ─────────────────────────────── */
11
12#[derive(Debug)]
13pub struct TrueFn;
14
15impl Function for TrueFn {
16    func_caps!(PURE);
17
18    fn name(&self) -> &'static str {
19        "TRUE"
20    }
21    fn min_args(&self) -> usize {
22        0
23    }
24
25    fn eval_scalar<'a, 'b>(
26        &self,
27        _args: &'a [ArgumentHandle<'a, 'b>],
28        _ctx: &dyn FunctionContext,
29    ) -> Result<LiteralValue, ExcelError> {
30        Ok(LiteralValue::Boolean(true))
31    }
32}
33
34/* ─────────────────────────── FALSE() ────────────────────────────── */
35
36#[derive(Debug)]
37pub struct FalseFn;
38
39impl Function for FalseFn {
40    func_caps!(PURE);
41
42    fn name(&self) -> &'static str {
43        "FALSE"
44    }
45    fn min_args(&self) -> usize {
46        0
47    }
48
49    fn eval_scalar<'a, 'b>(
50        &self,
51        _args: &'a [ArgumentHandle<'a, 'b>],
52        _ctx: &dyn FunctionContext,
53    ) -> Result<LiteralValue, ExcelError> {
54        Ok(LiteralValue::Boolean(false))
55    }
56}
57
58/* ─────────────────────────── AND() ──────────────────────────────── */
59
60#[derive(Debug)]
61pub struct AndFn;
62
63impl Function for AndFn {
64    func_caps!(PURE, REDUCTION, BOOL_ONLY, SHORT_CIRCUIT);
65
66    fn name(&self) -> &'static str {
67        "AND"
68    }
69    fn min_args(&self) -> usize {
70        1
71    }
72    fn variadic(&self) -> bool {
73        true
74    }
75    fn arg_schema(&self) -> &'static [ArgSchema] {
76        &ARG_ANY_ONE[..]
77    }
78
79    fn eval_scalar<'a, 'b>(
80        &self,
81        args: &'a [ArgumentHandle<'a, 'b>],
82        _ctx: &dyn FunctionContext,
83    ) -> Result<LiteralValue, ExcelError> {
84        let mut first_error: Option<LiteralValue> = None;
85        for h in args {
86            let it = h.lazy_values_owned()?;
87            for v in it {
88                match v {
89                    LiteralValue::Error(_) => {
90                        if first_error.is_none() {
91                            first_error = Some(v);
92                        }
93                    }
94                    LiteralValue::Empty => {
95                        return Ok(LiteralValue::Boolean(false));
96                    }
97                    LiteralValue::Boolean(b) => {
98                        if !b {
99                            return Ok(LiteralValue::Boolean(false));
100                        }
101                    }
102                    LiteralValue::Number(n) => {
103                        if n == 0.0 {
104                            return Ok(LiteralValue::Boolean(false));
105                        }
106                    }
107                    LiteralValue::Int(i) => {
108                        if i == 0 {
109                            return Ok(LiteralValue::Boolean(false));
110                        }
111                    }
112                    _ => {
113                        // Non-coercible (e.g., Text) → #VALUE! candidate with message
114                        if first_error.is_none() {
115                            first_error =
116                                Some(LiteralValue::Error(ExcelError::new_value().with_message(
117                                    "AND expects logical/numeric inputs; text is not coercible",
118                                )));
119                        }
120                    }
121                }
122            }
123        }
124        if let Some(err) = first_error {
125            return Ok(err);
126        }
127        Ok(LiteralValue::Boolean(true))
128    }
129}
130
131/* ─────────────────────────── OR() ───────────────────────────────── */
132
133#[derive(Debug)]
134pub struct OrFn;
135
136impl Function for OrFn {
137    func_caps!(PURE, REDUCTION, BOOL_ONLY, SHORT_CIRCUIT);
138
139    fn name(&self) -> &'static str {
140        "OR"
141    }
142    fn min_args(&self) -> usize {
143        1
144    }
145    fn variadic(&self) -> bool {
146        true
147    }
148    fn arg_schema(&self) -> &'static [ArgSchema] {
149        &ARG_ANY_ONE[..]
150    }
151
152    fn eval_scalar<'a, 'b>(
153        &self,
154        args: &'a [ArgumentHandle<'a, 'b>],
155        _ctx: &dyn FunctionContext,
156    ) -> Result<LiteralValue, ExcelError> {
157        let mut first_error: Option<LiteralValue> = None;
158        for h in args {
159            let it = h.lazy_values_owned()?;
160            for v in it {
161                match v {
162                    LiteralValue::Error(_) => {
163                        if first_error.is_none() {
164                            first_error = Some(v);
165                        }
166                    }
167                    LiteralValue::Empty => {
168                        // ignored
169                    }
170                    LiteralValue::Boolean(b) => {
171                        if b {
172                            return Ok(LiteralValue::Boolean(true));
173                        }
174                    }
175                    LiteralValue::Number(n) => {
176                        if n != 0.0 {
177                            return Ok(LiteralValue::Boolean(true));
178                        }
179                    }
180                    LiteralValue::Int(i) => {
181                        if i != 0 {
182                            return Ok(LiteralValue::Boolean(true));
183                        }
184                    }
185                    _ => {
186                        // Non-coercible → #VALUE! candidate with message
187                        if first_error.is_none() {
188                            first_error =
189                                Some(LiteralValue::Error(ExcelError::new_value().with_message(
190                                    "OR expects logical/numeric inputs; text is not coercible",
191                                )));
192                        }
193                    }
194                }
195            }
196        }
197        if let Some(err) = first_error {
198            return Ok(err);
199        }
200        Ok(LiteralValue::Boolean(false))
201    }
202}
203
204/* ─────────────────────────── IF() ───────────────────────────────── */
205
206#[derive(Debug)]
207pub struct IfFn;
208
209impl Function for IfFn {
210    func_caps!(PURE, SHORT_CIRCUIT);
211
212    fn name(&self) -> &'static str {
213        "IF"
214    }
215    fn min_args(&self) -> usize {
216        2
217    }
218    fn variadic(&self) -> bool {
219        true
220    }
221
222    fn arg_schema(&self) -> &'static [ArgSchema] {
223        use std::sync::LazyLock;
224        // Single variadic any schema so we can enforce precise 2 or 3 arity inside eval_scalar
225        static ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| vec![ArgSchema::any()]);
226        &ONE[..]
227    }
228
229    fn eval_scalar<'a, 'b>(
230        &self,
231        args: &'a [ArgumentHandle<'a, 'b>],
232        _ctx: &dyn FunctionContext,
233    ) -> Result<LiteralValue, ExcelError> {
234        if args.len() < 2 || args.len() > 3 {
235            return Ok(LiteralValue::Error(ExcelError::new_value().with_message(
236                format!("IF expects 2 or 3 arguments, got {}", args.len()),
237            )));
238        }
239
240        let condition = args[0].value()?;
241        let b = match condition.as_ref() {
242            LiteralValue::Boolean(b) => *b,
243            LiteralValue::Number(n) => *n != 0.0,
244            LiteralValue::Int(i) => *i != 0,
245            _ => {
246                return Ok(LiteralValue::Error(
247                    ExcelError::new_value().with_message("IF condition must be boolean or number"),
248                ));
249            }
250        };
251
252        if b {
253            args[1].value().map(|cow| cow.into_owned())
254        } else if let Some(arg) = args.get(2) {
255            arg.value().map(|cow| cow.into_owned())
256        } else {
257            Ok(LiteralValue::Boolean(false))
258        }
259    }
260}
261
262pub fn register_builtins() {
263    crate::function_registry::register_function(std::sync::Arc::new(TrueFn));
264    crate::function_registry::register_function(std::sync::Arc::new(FalseFn));
265    crate::function_registry::register_function(std::sync::Arc::new(AndFn));
266    crate::function_registry::register_function(std::sync::Arc::new(OrFn));
267    crate::function_registry::register_function(std::sync::Arc::new(IfFn));
268}
269
270/* ─────────────────────────── tests ─────────────────────────────── */
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::traits::{ArgumentHandle, DefaultFunctionContext};
276    use crate::{interpreter::Interpreter, test_workbook::TestWorkbook};
277    use formualizer_parse::LiteralValue;
278    use std::sync::{
279        Arc,
280        atomic::{AtomicUsize, Ordering},
281    };
282
283    #[derive(Debug)]
284    struct CountFn(Arc<AtomicUsize>);
285    impl Function for CountFn {
286        func_caps!(PURE);
287        fn name(&self) -> &'static str {
288            "COUNTING"
289        }
290        fn min_args(&self) -> usize {
291            0
292        }
293        fn eval_scalar<'a, 'b>(
294            &self,
295            _args: &'a [ArgumentHandle<'a, 'b>],
296            _ctx: &dyn FunctionContext,
297        ) -> Result<LiteralValue, ExcelError> {
298            self.0.fetch_add(1, Ordering::SeqCst);
299            Ok(LiteralValue::Boolean(true))
300        }
301    }
302
303    #[derive(Debug)]
304    struct ErrorFn(Arc<AtomicUsize>);
305    impl Function for ErrorFn {
306        func_caps!(PURE);
307        fn name(&self) -> &'static str {
308            "ERRORFN"
309        }
310        fn min_args(&self) -> usize {
311            0
312        }
313        fn eval_scalar<'a, 'b>(
314            &self,
315            _args: &'a [ArgumentHandle<'a, 'b>],
316            _ctx: &dyn FunctionContext,
317        ) -> Result<LiteralValue, ExcelError> {
318            self.0.fetch_add(1, Ordering::SeqCst);
319            Ok(LiteralValue::Error(ExcelError::new_value()))
320        }
321    }
322
323    fn interp(wb: &TestWorkbook) -> Interpreter<'_> {
324        wb.interpreter()
325    }
326
327    #[test]
328    fn test_true_false() {
329        let wb = TestWorkbook::new()
330            .with_function(std::sync::Arc::new(TrueFn))
331            .with_function(std::sync::Arc::new(FalseFn));
332
333        let ctx = interp(&wb);
334        let t = ctx.context.get_function("", "TRUE").unwrap();
335        let fctx = DefaultFunctionContext::new(ctx.context, None);
336        assert_eq!(
337            t.eval_scalar(&[], &fctx).unwrap(),
338            LiteralValue::Boolean(true)
339        );
340
341        let f = ctx.context.get_function("", "FALSE").unwrap();
342        assert_eq!(
343            f.eval_scalar(&[], &fctx).unwrap(),
344            LiteralValue::Boolean(false)
345        );
346    }
347
348    #[test]
349    fn test_and_or() {
350        let wb = TestWorkbook::new()
351            .with_function(std::sync::Arc::new(AndFn))
352            .with_function(std::sync::Arc::new(OrFn));
353        let ctx = interp(&wb);
354        let fctx = crate::traits::DefaultFunctionContext::new(ctx.context, None);
355
356        let and = ctx.context.get_function("", "AND").unwrap();
357        let or = ctx.context.get_function("", "OR").unwrap();
358        // Build ArgumentHandles manually: TRUE, 1, FALSE
359        let dummy_ast = formualizer_parse::parser::ASTNode::new(
360            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Boolean(true)),
361            None,
362        );
363        let dummy_ast_false = formualizer_parse::parser::ASTNode::new(
364            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Boolean(false)),
365            None,
366        );
367        let dummy_ast_one = formualizer_parse::parser::ASTNode::new(
368            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Int(1)),
369            None,
370        );
371        let hs = vec![
372            ArgumentHandle::new(&dummy_ast, &ctx),
373            ArgumentHandle::new(&dummy_ast_one, &ctx),
374        ];
375        assert_eq!(
376            and.eval_scalar(&hs, &fctx).unwrap(),
377            LiteralValue::Boolean(true)
378        );
379
380        let hs2 = vec![
381            ArgumentHandle::new(&dummy_ast_false, &ctx),
382            ArgumentHandle::new(&dummy_ast_one, &ctx),
383        ];
384        assert_eq!(
385            and.eval_scalar(&hs2, &fctx).unwrap(),
386            LiteralValue::Boolean(false)
387        );
388        assert_eq!(
389            or.eval_scalar(&hs2, &fctx).unwrap(),
390            LiteralValue::Boolean(true)
391        );
392    }
393
394    #[test]
395    fn and_short_circuits_on_false_without_evaluating_rest() {
396        let counter = Arc::new(AtomicUsize::new(0));
397        let wb = TestWorkbook::new()
398            .with_function(Arc::new(AndFn))
399            .with_function(Arc::new(CountFn(counter.clone())));
400        let ctx = interp(&wb);
401        let fctx = DefaultFunctionContext::new(ctx.context, None);
402        let and = ctx.context.get_function("", "AND").unwrap();
403
404        // Build args: FALSE, COUNTING()
405        let a_false = formualizer_parse::parser::ASTNode::new(
406            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Boolean(false)),
407            None,
408        );
409        let counting_call = formualizer_parse::parser::ASTNode::new(
410            formualizer_parse::parser::ASTNodeType::Function {
411                name: "COUNTING".into(),
412                args: vec![],
413            },
414            None,
415        );
416        let hs = vec![
417            ArgumentHandle::new(&a_false, &ctx),
418            ArgumentHandle::new(&counting_call, &ctx),
419        ];
420        let out = and.eval_scalar(&hs, &fctx).unwrap();
421        assert_eq!(out, LiteralValue::Boolean(false));
422        assert_eq!(
423            counter.load(Ordering::SeqCst),
424            0,
425            "COUNTING should not be evaluated"
426        );
427    }
428
429    #[test]
430    fn or_short_circuits_on_true_without_evaluating_rest() {
431        let counter = Arc::new(AtomicUsize::new(0));
432        let wb = TestWorkbook::new()
433            .with_function(Arc::new(OrFn))
434            .with_function(Arc::new(CountFn(counter.clone())));
435        let ctx = interp(&wb);
436        let fctx = DefaultFunctionContext::new(ctx.context, None);
437        let or = ctx.context.get_function("", "OR").unwrap();
438
439        // Build args: TRUE, COUNTING()
440        let a_true = formualizer_parse::parser::ASTNode::new(
441            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Boolean(true)),
442            None,
443        );
444        let counting_call = formualizer_parse::parser::ASTNode::new(
445            formualizer_parse::parser::ASTNodeType::Function {
446                name: "COUNTING".into(),
447                args: vec![],
448            },
449            None,
450        );
451        let hs = vec![
452            ArgumentHandle::new(&a_true, &ctx),
453            ArgumentHandle::new(&counting_call, &ctx),
454        ];
455        let out = or.eval_scalar(&hs, &fctx).unwrap();
456        assert_eq!(out, LiteralValue::Boolean(true));
457        assert_eq!(
458            counter.load(Ordering::SeqCst),
459            0,
460            "COUNTING should not be evaluated"
461        );
462    }
463
464    #[test]
465    fn or_range_arg_short_circuits_on_first_true_before_evaluating_next_arg() {
466        let counter = Arc::new(AtomicUsize::new(0));
467        let wb = TestWorkbook::new()
468            .with_function(Arc::new(OrFn))
469            .with_function(Arc::new(CountFn(counter.clone())));
470        let ctx = interp(&wb);
471        let fctx = DefaultFunctionContext::new(ctx.context, None);
472        let or = ctx.context.get_function("", "OR").unwrap();
473
474        // First arg is an array literal with first element 1 (truey), then zeros.
475        let arr = formualizer_parse::parser::ASTNode::new(
476            formualizer_parse::parser::ASTNodeType::Array(vec![
477                vec![formualizer_parse::parser::ASTNode::new(
478                    formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Int(1)),
479                    None,
480                )],
481                vec![formualizer_parse::parser::ASTNode::new(
482                    formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Int(0)),
483                    None,
484                )],
485            ]),
486            None,
487        );
488        let counting_call = formualizer_parse::parser::ASTNode::new(
489            formualizer_parse::parser::ASTNodeType::Function {
490                name: "COUNTING".into(),
491                args: vec![],
492            },
493            None,
494        );
495        let hs = vec![
496            ArgumentHandle::new(&arr, &ctx),
497            ArgumentHandle::new(&counting_call, &ctx),
498        ];
499        let out = or.eval_scalar(&hs, &fctx).unwrap();
500        assert_eq!(out, LiteralValue::Boolean(true));
501        assert_eq!(
502            counter.load(Ordering::SeqCst),
503            0,
504            "COUNTING should not be evaluated"
505        );
506    }
507
508    #[test]
509    fn and_returns_first_error_when_no_decisive_false() {
510        let err_counter = Arc::new(AtomicUsize::new(0));
511        let wb = TestWorkbook::new()
512            .with_function(Arc::new(AndFn))
513            .with_function(Arc::new(ErrorFn(err_counter.clone())));
514        let ctx = interp(&wb);
515        let fctx = DefaultFunctionContext::new(ctx.context, None);
516        let and = ctx.context.get_function("", "AND").unwrap();
517
518        // AND(1, ERRORFN(), 1) => #VALUE!
519        let one = formualizer_parse::parser::ASTNode::new(
520            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Int(1)),
521            None,
522        );
523        let errcall = formualizer_parse::parser::ASTNode::new(
524            formualizer_parse::parser::ASTNodeType::Function {
525                name: "ERRORFN".into(),
526                args: vec![],
527            },
528            None,
529        );
530        let hs = vec![
531            ArgumentHandle::new(&one, &ctx),
532            ArgumentHandle::new(&errcall, &ctx),
533            ArgumentHandle::new(&one, &ctx),
534        ];
535        let out = and.eval_scalar(&hs, &fctx).unwrap();
536        match out {
537            LiteralValue::Error(e) => assert_eq!(e.to_string(), "#VALUE!"),
538            _ => panic!("Expected error"),
539        }
540        assert_eq!(
541            err_counter.load(Ordering::SeqCst),
542            1,
543            "ERRORFN should be evaluated once"
544        );
545    }
546
547    #[test]
548    fn or_does_not_evaluate_error_after_true() {
549        let err_counter = Arc::new(AtomicUsize::new(0));
550        let wb = TestWorkbook::new()
551            .with_function(Arc::new(OrFn))
552            .with_function(Arc::new(ErrorFn(err_counter.clone())));
553        let ctx = interp(&wb);
554        let fctx = DefaultFunctionContext::new(ctx.context, None);
555        let or = ctx.context.get_function("", "OR").unwrap();
556
557        // OR(TRUE, ERRORFN()) => TRUE and ERRORFN not evaluated
558        let a_true = formualizer_parse::parser::ASTNode::new(
559            formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Boolean(true)),
560            None,
561        );
562        let errcall = formualizer_parse::parser::ASTNode::new(
563            formualizer_parse::parser::ASTNodeType::Function {
564                name: "ERRORFN".into(),
565                args: vec![],
566            },
567            None,
568        );
569        let hs = vec![
570            ArgumentHandle::new(&a_true, &ctx),
571            ArgumentHandle::new(&errcall, &ctx),
572        ];
573        let out = or.eval_scalar(&hs, &fctx).unwrap();
574        assert_eq!(out, LiteralValue::Boolean(true));
575        assert_eq!(
576            err_counter.load(Ordering::SeqCst),
577            0,
578            "ERRORFN should not be evaluated"
579        );
580    }
581}