Skip to main content

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