formualizer_eval/
interpreter.rs

1use crate::{
2    CellRef,
3    broadcast::{broadcast_shape, project_index},
4    coercion,
5    traits::{ArgumentHandle, DefaultFunctionContext, EvaluationContext},
6};
7use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
8use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
9// no Arc needed here after cache removal
10
11pub struct Interpreter<'a> {
12    pub context: &'a dyn EvaluationContext,
13    current_sheet: &'a str,
14    current_cell: Option<crate::CellRef>,
15}
16
17impl<'a> Interpreter<'a> {
18    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
19        Self {
20            context,
21            current_sheet,
22            current_cell: None,
23        }
24    }
25
26    pub fn new_with_cell(
27        context: &'a dyn EvaluationContext,
28        current_sheet: &'a str,
29        cell: crate::CellRef,
30    ) -> Self {
31        Self {
32            context,
33            current_sheet,
34            current_cell: Some(cell),
35        }
36    }
37
38    pub fn current_sheet(&self) -> &'a str {
39        self.current_sheet
40    }
41
42    pub fn resolve_range_view<'c>(
43        &'c self,
44        reference: &ReferenceType,
45        current_sheet: &str,
46    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
47        self.context.resolve_range_view(reference, current_sheet)
48    }
49
50    /// Evaluate an AST node in a reference context and return a ReferenceType.
51    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
52    /// and spill planning. Functions that can return references must set
53    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
54    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
55        match &node.node_type {
56            ASTNodeType::Reference { reference, .. } => Ok(reference.clone()),
57            ASTNodeType::Function { name, args } => {
58                if let Some(fun) = self.context.get_function("", name) {
59                    // Build handles; allow function to decide reference semantics
60                    let handles: Vec<ArgumentHandle> =
61                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
62                    let fctx = DefaultFunctionContext::new(self.context, None);
63                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
64                        res
65                    } else {
66                        Err(ExcelError::new(ExcelErrorKind::Ref)
67                            .with_message("Function does not return a reference"))
68                    }
69                } else {
70                    Err(ExcelError::new(ExcelErrorKind::Name)
71                        .with_message(format!("Unknown function: {name}")))
72                }
73            }
74            ASTNodeType::Array(_)
75            | ASTNodeType::UnaryOp { .. }
76            | ASTNodeType::BinaryOp { .. }
77            | ASTNodeType::Literal(_) => Err(ExcelError::new(ExcelErrorKind::Ref)
78                .with_message("Expression cannot be used as a reference")),
79        }
80    }
81
82    /* ===================  public  =================== */
83    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<LiteralValue, ExcelError> {
84        self.evaluate_ast_uncached(node)
85    }
86
87    fn evaluate_ast_uncached(&self, node: &ASTNode) -> Result<LiteralValue, ExcelError> {
88        // Plan-aware evaluation: build a plan for this node and execute accordingly.
89        // Provide the planner with a lightweight range-dimension probe and function lookup
90        // so it can select chunked reduction and arg-parallel strategies where appropriate.
91        let current_sheet = self.current_sheet.to_string();
92        let range_probe = |reference: &ReferenceType| -> Option<(u32, u32)> {
93            // Mirror Engine::resolve_range_storage bound normalization without materialising
94            use formualizer_parse::parser::ReferenceType as RT;
95            match reference {
96                RT::Range {
97                    sheet,
98                    start_row,
99                    start_col,
100                    end_row,
101                    end_col,
102                } => {
103                    let sheet_name = sheet.as_deref().unwrap_or(&current_sheet);
104                    // Start with provided values, fill None from used-region or sheet bounds.
105                    let mut sr = *start_row;
106                    let mut sc = *start_col;
107                    let mut er = *end_row;
108                    let mut ec = *end_col;
109
110                    // Column-only: rows are None on both ends
111                    if sr.is_none() && er.is_none() {
112                        // Full-column reference: anchor at row 1 for alignment across columns
113                        let scv = sc.unwrap_or(1);
114                        let ecv = ec.unwrap_or(scv);
115                        sr = Some(1);
116                        if let Some((_, max_r)) =
117                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
118                        {
119                            er = Some(max_r);
120                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
121                            er = Some(max_rows);
122                        }
123                    }
124
125                    // Row-only: cols are None on both ends
126                    if sc.is_none() && ec.is_none() {
127                        // Full-row reference: anchor at column 1 for alignment across rows
128                        let srv = sr.unwrap_or(1);
129                        let erv = er.unwrap_or(srv);
130                        sc = Some(1);
131                        if let Some((_, max_c)) =
132                            self.context.used_cols_for_rows(sheet_name, srv, erv)
133                        {
134                            ec = Some(max_c);
135                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
136                            ec = Some(max_cols);
137                        }
138                    }
139
140                    // Partially bounded (e.g., A1:A or A:A10)
141                    if sr.is_some() && er.is_none() {
142                        let scv = sc.unwrap_or(1);
143                        let ecv = ec.unwrap_or(scv);
144                        if let Some((_, max_r)) =
145                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
146                        {
147                            er = Some(max_r);
148                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
149                            er = Some(max_rows);
150                        }
151                    }
152                    if er.is_some() && sr.is_none() {
153                        // Open start: anchor at row 1
154                        sr = Some(1);
155                    }
156                    if sc.is_some() && ec.is_none() {
157                        let srv = sr.unwrap_or(1);
158                        let erv = er.unwrap_or(srv);
159                        if let Some((_, max_c)) =
160                            self.context.used_cols_for_rows(sheet_name, srv, erv)
161                        {
162                            ec = Some(max_c);
163                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
164                            ec = Some(max_cols);
165                        }
166                    }
167                    if ec.is_some() && sc.is_none() {
168                        // Open start: anchor at column 1
169                        sc = Some(1);
170                    }
171
172                    let sr = sr.unwrap_or(1);
173                    let sc = sc.unwrap_or(1);
174                    let er = er.unwrap_or(sr.saturating_sub(1));
175                    let ec = ec.unwrap_or(sc.saturating_sub(1));
176                    if er < sr || ec < sc {
177                        return Some((0, 0));
178                    }
179                    Some((er.saturating_sub(sr) + 1, ec.saturating_sub(sc) + 1))
180                }
181                RT::Cell { .. } => Some((1, 1)),
182                _ => None,
183            }
184        };
185        let fn_lookup = |ns: &str, name: &str| self.context.get_function(ns, name);
186
187        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
188            .with_range_probe(&range_probe)
189            .with_function_lookup(&fn_lookup);
190        let plan = planner.plan(node);
191        self.eval_with_plan(node, &plan.root)
192    }
193
194    fn eval_with_plan(
195        &self,
196        node: &ASTNode,
197        plan_node: &crate::planner::PlanNode,
198    ) -> Result<LiteralValue, ExcelError> {
199        match &node.node_type {
200            ASTNodeType::Literal(v) => Ok(v.clone()),
201            ASTNodeType::Reference { reference, .. } => self.eval_reference(reference),
202            ASTNodeType::UnaryOp { op, expr } => {
203                // For now, reuse existing unary implementation (which recurses).
204                // In a later phase, we can map plan_node.children[0].
205                self.eval_unary(op, expr)
206            }
207            ASTNodeType::BinaryOp { op, left, right } => self.eval_binary(op, left, right),
208            ASTNodeType::Function { name, args } => {
209                let strategy = plan_node.strategy;
210                if let Some(fun) = self.context.get_function("", name) {
211                    use crate::function::FnCaps;
212                    use crate::planner::ExecStrategy;
213                    let caps = fun.caps();
214
215                    // Short-circuit or volatile: always sequential
216                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
217                        return self.eval_function(name, args);
218                    }
219
220                    // Chunked reduce for windowed functions
221                    if matches!(strategy, ExecStrategy::ChunkedReduce)
222                        && caps.contains(FnCaps::WINDOWED)
223                    {
224                        let handles: Vec<ArgumentHandle> =
225                            args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
226                        let fctx = DefaultFunctionContext::new(self.context, self.current_cell);
227                        let mut w = crate::window_ctx::SimpleWindowCtx::new(
228                            &handles,
229                            &fctx,
230                            crate::window_ctx::WindowSpec::default(),
231                        );
232                        if let Some(res) = fun.eval_window(&mut w) {
233                            return res;
234                        }
235                        // Fallback to scalar/dispatch if window not implemented
236                        return self.eval_function(name, args);
237                    }
238
239                    // Arg-parallel: prewarm subexpressions and then dispatch
240                    if matches!(strategy, ExecStrategy::ArgParallel)
241                        && caps.contains(FnCaps::PARALLEL_ARGS)
242                    {
243                        // Sequential prewarm of subexpressions (safe without Sync bounds)
244                        for arg in args {
245                            match &arg.node_type {
246                                ASTNodeType::Reference { reference, .. } => {
247                                    let _ = self
248                                        .context
249                                        .resolve_range_view(reference, self.current_sheet);
250                                }
251                                _ => {
252                                    let _ = self.evaluate_ast(arg);
253                                }
254                            }
255                        }
256                        return self.eval_function(name, args);
257                    }
258
259                    // Default path
260                    return self.eval_function(name, args);
261                }
262                self.eval_function(name, args)
263            }
264            ASTNodeType::Array(rows) => self.eval_array_literal(rows),
265        }
266    }
267
268    /* ===================  reference  =================== */
269    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
270        let view = self
271            .context
272            .resolve_range_view(reference, self.current_sheet)?;
273
274        match reference {
275            ReferenceType::Cell { .. } => {
276                // For a single cell reference, just return the value.
277                Ok(view.as_1x1().unwrap_or(LiteralValue::Empty))
278            }
279            _ => {
280                // For ranges, materialize into an array.
281                let (rows, cols) = view.dims();
282                let mut data = Vec::with_capacity(rows);
283
284                view.for_each_row(&mut |row| {
285                    let row_data: Vec<LiteralValue> = (0..cols)
286                        .map(|c| row.get(c).cloned().unwrap_or(LiteralValue::Empty))
287                        .collect();
288                    data.push(row_data);
289                    Ok(())
290                })?;
291
292                if data.len() == 1 && data[0].len() == 1 {
293                    Ok(data[0][0].clone())
294                } else {
295                    Ok(LiteralValue::Array(data))
296                }
297            }
298        }
299    }
300
301    /* ===================  unary ops  =================== */
302    fn eval_unary(&self, op: &str, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
303        let v = self.evaluate_ast(expr)?;
304        match v {
305            LiteralValue::Array(arr) => {
306                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
307            }
308            other => self.eval_unary_scalar(op, other),
309        }
310    }
311
312    fn eval_unary_scalar(&self, op: &str, v: LiteralValue) -> Result<LiteralValue, ExcelError> {
313        match op {
314            "+" => self.apply_number_unary(v, |n| n),
315            "-" => self.apply_number_unary(v, |n| -n),
316            "%" => self.apply_number_unary(v, |n| n / 100.0),
317            _ => {
318                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
319            }
320        }
321    }
322
323    fn apply_number_unary<F>(&self, v: LiteralValue, f: F) -> Result<LiteralValue, ExcelError>
324    where
325        F: Fn(f64) -> f64,
326    {
327        match crate::coercion::to_number_lenient_with_locale(&v, &self.context.locale()) {
328            Ok(n) => match crate::coercion::sanitize_numeric(f(n)) {
329                Ok(n2) => Ok(LiteralValue::Number(n2)),
330                Err(e) => Ok(LiteralValue::Error(e)),
331            },
332            Err(e) => Ok(LiteralValue::Error(e)),
333        }
334    }
335
336    /* ===================  binary ops  =================== */
337    fn eval_binary(
338        &self,
339        op: &str,
340        left: &ASTNode,
341        right: &ASTNode,
342    ) -> Result<LiteralValue, ExcelError> {
343        // Comparisons use dedicated path.
344        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
345            let l = self.evaluate_ast(left)?;
346            let r = self.evaluate_ast(right)?;
347            return self.compare(op, l, r);
348        }
349
350        let l_val = self.evaluate_ast(left)?;
351        let r_val = self.evaluate_ast(right)?;
352
353        match op {
354            "+" => self.numeric_binary(l_val, r_val, |a, b| a + b),
355            "-" => self.numeric_binary(l_val, r_val, |a, b| a - b),
356            "*" => self.numeric_binary(l_val, r_val, |a, b| a * b),
357            "/" => self.divide(l_val, r_val),
358            "^" => self.power(l_val, r_val),
359            "&" => Ok(LiteralValue::Text(format!(
360                "{}{}",
361                crate::coercion::to_text_invariant(&l_val),
362                crate::coercion::to_text_invariant(&r_val)
363            ))),
364            ":" => {
365                // Compute a combined reference; in value context return #REF! for now.
366                let lref = self.evaluate_ast_as_reference(left)?;
367                let rref = self.evaluate_ast_as_reference(right)?;
368                match crate::reference::combine_references(&lref, &rref) {
369                    Ok(_r) => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
370                        "Reference produced by ':' cannot be used directly as a value",
371                    )),
372                    Err(e) => Ok(LiteralValue::Error(e)),
373                }
374            }
375            _ => {
376                Err(ExcelError::new(ExcelErrorKind::NImpl)
377                    .with_message(format!("Binary op '{op}'")))
378            }
379        }
380    }
381
382    /* ===================  function calls  =================== */
383    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
384        if let Some(fun) = self.context.get_function("", name) {
385            let handles: Vec<ArgumentHandle> =
386                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
387            // Use the function's built-in dispatch method with a narrow FunctionContext
388            let fctx = DefaultFunctionContext::new(self.context, self.current_cell);
389            fun.dispatch(&handles, &fctx)
390        } else {
391            // Include the function name in the error message for better debugging
392            Ok(LiteralValue::Error(
393                ExcelError::new(ExcelErrorKind::Name)
394                    .with_message(format!("Unknown function: {name}")),
395            ))
396        }
397    }
398
399    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
400        DefaultFunctionContext::new(self.context, cell_ref.cloned())
401    }
402
403    // Test-only helpers removed: interpreter no longer maintains subexpression cache
404    // owned range cache removed in RangeView migration
405
406    /* ===================  array literal  =================== */
407    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
408        let mut out = Vec::with_capacity(rows.len());
409        for row in rows {
410            let mut r = Vec::with_capacity(row.len());
411            for cell in row {
412                r.push(self.evaluate_ast(cell)?);
413            }
414            out.push(r);
415        }
416        Ok(LiteralValue::Array(out))
417    }
418
419    /* ===================  helpers  =================== */
420    fn numeric_binary<F>(
421        &self,
422        left: LiteralValue,
423        right: LiteralValue,
424        f: F,
425    ) -> Result<LiteralValue, ExcelError>
426    where
427        F: Fn(f64, f64) -> f64 + Copy,
428    {
429        self.broadcast_apply(left, right, |l, r| {
430            let a = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
431            let b = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
432            match (a, b) {
433                (Ok(a), Ok(b)) => match crate::coercion::sanitize_numeric(f(a, b)) {
434                    Ok(n2) => Ok(LiteralValue::Number(n2)),
435                    Err(e) => Ok(LiteralValue::Error(e)),
436                },
437                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
438            }
439        })
440    }
441
442    fn divide(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
443        self.broadcast_apply(left, right, |l, r| {
444            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
445            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
446            let (a, b) = match (ln, rn) {
447                (Ok(a), Ok(b)) => (a, b),
448                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
449            };
450            if b == 0.0 {
451                return Ok(LiteralValue::Error(ExcelError::from_error_string(
452                    "#DIV/0!",
453                )));
454            }
455            match crate::coercion::sanitize_numeric(a / b) {
456                Ok(n) => Ok(LiteralValue::Number(n)),
457                Err(e) => Ok(LiteralValue::Error(e)),
458            }
459        })
460    }
461
462    fn power(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
463        self.broadcast_apply(left, right, |l, r| {
464            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
465            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
466            let (a, b) = match (ln, rn) {
467                (Ok(a), Ok(b)) => (a, b),
468                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
469            };
470            // Excel domain: negative base with non-integer exponent -> #NUM!
471            if a < 0.0 && b.fract() != 0.0 {
472                return Ok(LiteralValue::Error(ExcelError::new_num()));
473            }
474            match crate::coercion::sanitize_numeric(a.powf(b)) {
475                Ok(n) => Ok(LiteralValue::Number(n)),
476                Err(e) => Ok(LiteralValue::Error(e)),
477            }
478        })
479    }
480
481    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
482    where
483        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
484    {
485        let mut out = Vec::with_capacity(arr.len());
486        for row in arr {
487            let mut new_row = Vec::with_capacity(row.len());
488            for cell in row {
489                new_row.push(match f(cell) {
490                    Ok(v) => v,
491                    Err(e) => LiteralValue::Error(e),
492                });
493            }
494            out.push(new_row);
495        }
496        Ok(LiteralValue::Array(out))
497    }
498
499    fn combine_arrays<F>(
500        &self,
501        l: Vec<Vec<LiteralValue>>,
502        r: Vec<Vec<LiteralValue>>,
503        f: F,
504    ) -> Result<LiteralValue, ExcelError>
505    where
506        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
507    {
508        // Use strict broadcasting across dimensions
509        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
510        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
511        let target = match broadcast_shape(&[l_shape, r_shape]) {
512            Ok(s) => s,
513            Err(e) => return Ok(LiteralValue::Error(e)),
514        };
515
516        let mut out = Vec::with_capacity(target.0);
517        for i in 0..target.0 {
518            let mut row = Vec::with_capacity(target.1);
519            for j in 0..target.1 {
520                let (li, lj) = project_index((i, j), l_shape);
521                let (ri, rj) = project_index((i, j), r_shape);
522                let lv = l
523                    .get(li)
524                    .and_then(|r| r.get(lj))
525                    .cloned()
526                    .unwrap_or(LiteralValue::Empty);
527                let rv = r
528                    .get(ri)
529                    .and_then(|r| r.get(rj))
530                    .cloned()
531                    .unwrap_or(LiteralValue::Empty);
532                row.push(match f(lv, rv) {
533                    Ok(v) => v,
534                    Err(e) => LiteralValue::Error(e),
535                });
536            }
537            out.push(row);
538        }
539        Ok(LiteralValue::Array(out))
540    }
541
542    fn broadcast_apply<F>(
543        &self,
544        left: LiteralValue,
545        right: LiteralValue,
546        f: F,
547    ) -> Result<LiteralValue, ExcelError>
548    where
549        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
550    {
551        use LiteralValue::*;
552        match (left, right) {
553            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
554            (Array(arr), v) => {
555                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
556                let shape_r = (1usize, 1usize);
557                let target = match broadcast_shape(&[shape_l, shape_r]) {
558                    Ok(s) => s,
559                    Err(e) => return Ok(LiteralValue::Error(e)),
560                };
561                let mut out = Vec::with_capacity(target.0);
562                for i in 0..target.0 {
563                    let mut row = Vec::with_capacity(target.1);
564                    for j in 0..target.1 {
565                        let (li, lj) = project_index((i, j), shape_l);
566                        let lv = arr
567                            .get(li)
568                            .and_then(|r| r.get(lj))
569                            .cloned()
570                            .unwrap_or(LiteralValue::Empty);
571                        row.push(match f(lv, v.clone()) {
572                            Ok(vv) => vv,
573                            Err(e) => LiteralValue::Error(e),
574                        });
575                    }
576                    out.push(row);
577                }
578                Ok(LiteralValue::Array(out))
579            }
580            (v, Array(arr)) => {
581                let shape_l = (1usize, 1usize);
582                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
583                let target = match broadcast_shape(&[shape_l, shape_r]) {
584                    Ok(s) => s,
585                    Err(e) => return Ok(LiteralValue::Error(e)),
586                };
587                let mut out = Vec::with_capacity(target.0);
588                for i in 0..target.0 {
589                    let mut row = Vec::with_capacity(target.1);
590                    for j in 0..target.1 {
591                        let (ri, rj) = project_index((i, j), shape_r);
592                        let rv = arr
593                            .get(ri)
594                            .and_then(|r| r.get(rj))
595                            .cloned()
596                            .unwrap_or(LiteralValue::Empty);
597                        row.push(match f(v.clone(), rv) {
598                            Ok(vv) => vv,
599                            Err(e) => LiteralValue::Error(e),
600                        });
601                    }
602                    out.push(row);
603                }
604                Ok(LiteralValue::Array(out))
605            }
606            (l, r) => f(l, r),
607        }
608    }
609
610    /* ---------- coercion helpers ---------- */
611    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
612        coercion::to_number_lenient(v)
613    }
614
615    fn coerce_text(&self, v: &LiteralValue) -> String {
616        coercion::to_text_invariant(v)
617    }
618
619    /* ---------- comparison ---------- */
620    fn compare(
621        &self,
622        op: &str,
623        left: LiteralValue,
624        right: LiteralValue,
625    ) -> Result<LiteralValue, ExcelError> {
626        use LiteralValue::*;
627        if matches!(left, Error(_)) {
628            return Ok(left);
629        }
630        if matches!(right, Error(_)) {
631            return Ok(right);
632        }
633
634        // arrays: element‑wise with broadcasting
635        match (left, right) {
636            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
637            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
638            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
639            (l, r) => {
640                let res = match (l, r) {
641                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
642                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
643                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
644                    (Boolean(a), Boolean(b)) => {
645                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
646                    }
647                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
648                    (a, b) => {
649                        // fallback to numeric coercion or text compare
650                        let an = crate::coercion::to_number_lenient_with_locale(
651                            &a,
652                            &self.context.locale(),
653                        )
654                        .ok();
655                        let bn = crate::coercion::to_number_lenient_with_locale(
656                            &b,
657                            &self.context.locale(),
658                        )
659                        .ok();
660                        if let (Some(a), Some(b)) = (an, bn) {
661                            self.cmp_f64(a, b, op)
662                        } else {
663                            self.cmp_text(
664                                &crate::coercion::to_text_invariant(&a),
665                                &crate::coercion::to_text_invariant(&b),
666                                op,
667                            )
668                        }
669                    }
670                };
671                Ok(LiteralValue::Boolean(res))
672            }
673        }
674    }
675
676    fn cmp_f64(&self, a: f64, b: f64, op: &str) -> bool {
677        match op {
678            "=" => a == b,
679            "<>" => a != b,
680            ">" => a > b,
681            "<" => a < b,
682            ">=" => a >= b,
683            "<=" => a <= b,
684            _ => unreachable!(),
685        }
686    }
687    fn cmp_text(&self, a: &str, b: &str, op: &str) -> bool {
688        let loc = self.context.locale();
689        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
690        self.cmp_f64(
691            a.cmp(&b) as i32 as f64,
692            0.0,
693            match op {
694                "=" => "=",
695                "<>" => "<>",
696                ">" => ">",
697                "<" => "<",
698                ">=" => ">=",
699                "<=" => "<=",
700                _ => unreachable!(),
701            },
702        )
703    }
704}