Skip to main content

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};
9use rustc_hash::FxHashMap;
10use std::sync::Arc;
11
12use crate::engine::arena::ast::SheetKey;
13use crate::engine::arena::{AstNodeData, AstNodeId, CompactRefType, DataStore};
14use crate::engine::sheet_registry::SheetRegistry;
15
16#[derive(Clone)]
17pub enum LocalBinding {
18    Value(LiteralValue),
19    Callable(Arc<dyn crate::traits::CustomCallable>),
20}
21
22#[derive(Clone, Default)]
23pub struct LocalEnv {
24    head: Option<Arc<EnvFrame>>,
25}
26
27#[derive(Clone)]
28struct EnvFrame {
29    parent: Option<Arc<EnvFrame>>,
30    bindings: FxHashMap<String, LocalBinding>,
31}
32
33impl LocalEnv {
34    #[inline(always)]
35    pub fn is_empty(&self) -> bool {
36        self.head.is_none()
37    }
38
39    fn norm(name: &str) -> String {
40        name.to_ascii_uppercase()
41    }
42
43    pub fn lookup(&self, name: &str) -> Option<LocalBinding> {
44        self.head.as_ref()?;
45        let key = Self::norm(name);
46        let mut cur = self.head.as_ref().cloned();
47        while let Some(frame) = cur {
48            if let Some(v) = frame.bindings.get(&key) {
49                return Some(v.clone());
50            }
51            cur = frame.parent.clone();
52        }
53        None
54    }
55
56    pub fn with_binding(&self, name: &str, value: LocalBinding) -> Self {
57        let mut bindings = FxHashMap::default();
58        bindings.insert(Self::norm(name), value);
59        Self {
60            head: Some(Arc::new(EnvFrame {
61                parent: self.head.clone(),
62                bindings,
63            })),
64        }
65    }
66}
67
68pub struct Interpreter<'a> {
69    pub context: &'a dyn EvaluationContext,
70    current_sheet: &'a str,
71    current_cell: Option<crate::CellRef>,
72    local_env: LocalEnv,
73}
74
75impl<'a> Interpreter<'a> {
76    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
77        Self {
78            context,
79            current_sheet,
80            current_cell: None,
81            local_env: LocalEnv::default(),
82        }
83    }
84
85    pub fn new_with_cell(
86        context: &'a dyn EvaluationContext,
87        current_sheet: &'a str,
88        cell: crate::CellRef,
89    ) -> Self {
90        Self {
91            context,
92            current_sheet,
93            current_cell: Some(cell),
94            local_env: LocalEnv::default(),
95        }
96    }
97
98    pub fn current_sheet(&self) -> &'a str {
99        self.current_sheet
100    }
101
102    pub fn local_env(&self) -> &LocalEnv {
103        &self.local_env
104    }
105
106    pub fn with_local_env(&self, env: LocalEnv) -> Self {
107        Self {
108            context: self.context,
109            current_sheet: self.current_sheet,
110            current_cell: self.current_cell,
111            local_env: env,
112        }
113    }
114
115    fn resolve_local_reference(
116        &self,
117        reference: &ReferenceType,
118    ) -> Option<crate::traits::CalcValue<'a>> {
119        if self.local_env.is_empty() {
120            return None;
121        }
122        let name = match reference {
123            ReferenceType::NamedRange(name) => name,
124            _ => return None,
125        };
126        match self.local_env.lookup(name)? {
127            LocalBinding::Value(v) => Some(crate::traits::CalcValue::Scalar(v)),
128            LocalBinding::Callable(c) => Some(crate::traits::CalcValue::Callable(c)),
129        }
130    }
131
132    fn resolve_local_callable(&self, name: &str) -> Option<Arc<dyn crate::traits::CustomCallable>> {
133        if self.local_env.is_empty() {
134            return None;
135        }
136        match self.local_env.lookup(name)? {
137            LocalBinding::Callable(c) => Some(c),
138            LocalBinding::Value(_) => None,
139        }
140    }
141
142    pub fn resolve_local_name(&self, name: &str) -> Option<LocalBinding> {
143        self.local_env.lookup(name)
144    }
145
146    pub fn resolve_range_view<'c>(
147        &'c self,
148        reference: &ReferenceType,
149        current_sheet: &str,
150    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
151        self.context.resolve_range_view(reference, current_sheet)
152    }
153
154    /// Evaluate an AST node in a reference context and return a ReferenceType.
155    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
156    /// and spill planning. Functions that can return references must set
157    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
158    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
159        match &node.node_type {
160            ASTNodeType::Reference { reference, .. } => Ok(reference.clone()),
161            ASTNodeType::Function { name, args } => {
162                if let Some(fun) = self.context.get_function("", name) {
163                    // Build handles; allow function to decide reference semantics
164                    let handles: Vec<ArgumentHandle> =
165                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
166                    let fctx = DefaultFunctionContext::new_with_sheet(
167                        self.context,
168                        None,
169                        self.current_sheet,
170                    );
171                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
172                        res
173                    } else {
174                        Err(ExcelError::new(ExcelErrorKind::Ref)
175                            .with_message("Function does not return a reference"))
176                    }
177                } else {
178                    Err(ExcelError::new(ExcelErrorKind::Name)
179                        .with_message(format!("Unknown function: {name}")))
180                }
181            }
182            ASTNodeType::BinaryOp { op, left, right } if op == ":" => {
183                let lref = self.evaluate_ast_as_reference(left)?;
184                let rref = self.evaluate_ast_as_reference(right)?;
185                crate::reference::combine_references(&lref, &rref)
186            }
187            ASTNodeType::Array(_)
188            | ASTNodeType::UnaryOp { .. }
189            | ASTNodeType::BinaryOp { .. }
190            | ASTNodeType::Call { .. }
191            | ASTNodeType::Literal(_) => Err(ExcelError::new(ExcelErrorKind::Ref)
192                .with_message("Expression cannot be used as a reference")),
193        }
194    }
195
196    pub(crate) fn evaluate_arena_ast_as_reference(
197        &self,
198        node_id: AstNodeId,
199        data_store: &DataStore,
200        sheet_registry: &SheetRegistry,
201    ) -> Result<ReferenceType, ExcelError> {
202        let node = data_store.get_node(node_id).ok_or_else(|| {
203            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
204        })?;
205
206        match node {
207            AstNodeData::Reference { ref_type, .. } => {
208                Ok(data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry))
209            }
210            AstNodeData::Function { name_id, .. } => {
211                let name = data_store.resolve_ast_string(*name_id);
212                let fun = self.context.get_function("", name).ok_or_else(|| {
213                    ExcelError::new(ExcelErrorKind::Name)
214                        .with_message(format!("Unknown function: {name}"))
215                })?;
216
217                let args = data_store.get_args(node_id).ok_or_else(|| {
218                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
219                })?;
220
221                let handles: Vec<ArgumentHandle> = args
222                    .iter()
223                    .copied()
224                    .map(|arg_id| {
225                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
226                    })
227                    .collect();
228
229                let fctx =
230                    DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
231
232                fun.eval_reference(&handles, &fctx).ok_or_else(|| {
233                    ExcelError::new(ExcelErrorKind::Ref)
234                        .with_message("Function does not return a reference")
235                })?
236            }
237            AstNodeData::BinaryOp {
238                op_id,
239                left_id,
240                right_id,
241            } => {
242                let op = data_store.resolve_ast_string(*op_id);
243                if op != ":" {
244                    return Err(ExcelError::new(ExcelErrorKind::Ref)
245                        .with_message("Expression cannot be used as a reference"));
246                }
247                let lref =
248                    self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
249                let rref =
250                    self.evaluate_arena_ast_as_reference(*right_id, data_store, sheet_registry)?;
251                crate::reference::combine_references(&lref, &rref)
252            }
253            _ => Err(ExcelError::new(ExcelErrorKind::Ref)
254                .with_message("Expression cannot be used as a reference")),
255        }
256    }
257
258    /* ===================  public  =================== */
259    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
260        self.evaluate_ast_uncached(node)
261    }
262
263    pub(crate) fn evaluate_arena_ast(
264        &self,
265        node_id: AstNodeId,
266        data_store: &DataStore,
267        sheet_registry: &SheetRegistry,
268    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
269        let node = data_store.get_node(node_id).ok_or_else(|| {
270            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
271        })?;
272
273        match node {
274            AstNodeData::Literal(vref) => Ok(crate::traits::CalcValue::Scalar(
275                data_store.retrieve_value(*vref),
276            )),
277            AstNodeData::Reference { ref_type, .. } => {
278                if let CompactRefType::Cell {
279                    sheet, row, col, ..
280                } = ref_type
281                    && *row > 0
282                    && *col > 0
283                {
284                    let sheet_name = match sheet {
285                        Some(SheetKey::Id(id)) => Some(sheet_registry.name(*id)),
286                        Some(SheetKey::Name(name_id)) => {
287                            Some(data_store.resolve_ast_string(*name_id))
288                        }
289                        None => None,
290                    };
291                    let value = self.context.resolve_cell_reference_value(
292                        sheet_name,
293                        *row,
294                        *col,
295                        self.current_sheet,
296                    )?;
297                    Ok(crate::traits::CalcValue::Scalar(value))
298                } else {
299                    let reference =
300                        data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
301                    if let Some(local) = self.resolve_local_reference(&reference) {
302                        return Ok(local);
303                    }
304                    self.eval_reference_to_calc(&reference)
305                }
306            }
307            AstNodeData::UnaryOp { op_id, expr_id } => {
308                let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
309
310                let op = data_store.resolve_ast_string(*op_id);
311                if op == "@" {
312                    // Prefer reference-aware implicit intersection so we don't depend on
313                    // RangeView absolute coordinates (important for lightweight test contexts).
314                    if let Some(AstNodeData::Reference { ref_type, .. }) =
315                        data_store.get_node(*expr_id)
316                    {
317                        let reference = data_store
318                            .reconstruct_reference_type_for_eval(ref_type, sheet_registry);
319                        let v = self.implicit_intersection_from_reference(&reference);
320                        return Ok(crate::traits::CalcValue::Scalar(v));
321                    }
322
323                    let v = self.eval_implicit_intersection_calc(expr);
324                    return Ok(crate::traits::CalcValue::Scalar(v));
325                }
326                // For now, materialize for operators. Future: virtual range ops.
327                let v = expr.into_literal();
328                match v {
329                    LiteralValue::Array(arr) => self
330                        .map_array(arr, |cell| self.eval_unary_scalar(op, cell))
331                        .map(crate::traits::CalcValue::Scalar),
332                    other => self
333                        .eval_unary_scalar(op, other)
334                        .map(crate::traits::CalcValue::Scalar),
335                }
336            }
337            AstNodeData::BinaryOp {
338                op_id,
339                left_id,
340                right_id,
341            } => {
342                let op = data_store.resolve_ast_string(*op_id);
343                if op == ":" {
344                    let lref =
345                        self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
346                    let rref = self.evaluate_arena_ast_as_reference(
347                        *right_id,
348                        data_store,
349                        sheet_registry,
350                    )?;
351                    return match crate::reference::combine_references(&lref, &rref) {
352                        Ok(_r) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
353                            ExcelError::new(ExcelErrorKind::Ref).with_message(
354                                "Reference produced by ':' cannot be used directly as a value",
355                            ),
356                        ))),
357                        Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
358                    };
359                }
360
361                let left = self
362                    .evaluate_arena_ast(*left_id, data_store, sheet_registry)?
363                    .into_literal();
364                let right = self
365                    .evaluate_arena_ast(*right_id, data_store, sheet_registry)?
366                    .into_literal();
367
368                if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
369                    return self
370                        .compare(op, left, right)
371                        .map(crate::traits::CalcValue::Scalar);
372                }
373
374                match op {
375                    "+" => self
376                        .add_sub_date_aware('+', left, right)
377                        .map(crate::traits::CalcValue::Scalar),
378                    "-" => self
379                        .add_sub_date_aware('-', left, right)
380                        .map(crate::traits::CalcValue::Scalar),
381                    "*" => self
382                        .numeric_binary(left, right, |a, b| a * b)
383                        .map(crate::traits::CalcValue::Scalar),
384                    "/" => self
385                        .divide(left, right)
386                        .map(crate::traits::CalcValue::Scalar),
387                    "^" => self
388                        .power(left, right)
389                        .map(crate::traits::CalcValue::Scalar),
390                    "&" => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
391                        format!(
392                            "{}{}",
393                            crate::coercion::to_text_invariant(&left),
394                            crate::coercion::to_text_invariant(&right)
395                        ),
396                    ))),
397                    _ => Err(ExcelError::new(ExcelErrorKind::NImpl)
398                        .with_message(format!("Binary op '{op}'"))),
399                }
400            }
401            AstNodeData::Array { .. } => {
402                let (rows, cols, elements) =
403                    data_store.get_array_elems(node_id).ok_or_else(|| {
404                        ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
405                    })?;
406
407                let rows_usize = rows as usize;
408                let cols_usize = cols as usize;
409                let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows_usize);
410                for r in 0..rows_usize {
411                    let mut row = Vec::with_capacity(cols_usize);
412                    for c in 0..cols_usize {
413                        let idx = r * cols_usize + c;
414                        if let Some(&elem_id) = elements.get(idx) {
415                            row.push(
416                                self.evaluate_arena_ast(elem_id, data_store, sheet_registry)?
417                                    .into_literal(),
418                            );
419                        }
420                    }
421                    out.push(row);
422                }
423
424                Ok(crate::traits::CalcValue::Range(
425                    crate::engine::range_view::RangeView::from_owned_rows(
426                        out,
427                        self.context.date_system(),
428                    ),
429                ))
430            }
431            AstNodeData::Function { name_id, .. } => {
432                let name = data_store.resolve_ast_string(*name_id);
433                let args = data_store.get_args(node_id).ok_or_else(|| {
434                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
435                })?;
436
437                if let Some(fun) = self.context.get_function("", name) {
438                    let handles: Vec<ArgumentHandle> = args
439                        .iter()
440                        .copied()
441                        .map(|arg_id| {
442                            ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
443                        })
444                        .collect();
445
446                    let fctx = DefaultFunctionContext::new_with_sheet(
447                        self.context,
448                        self.current_cell,
449                        self.current_sheet,
450                    );
451
452                    return fun.dispatch(&handles, &fctx);
453                }
454
455                if let Some(callable) = self.resolve_local_callable(name) {
456                    let mut eval_args = Vec::with_capacity(args.len());
457                    for arg_id in args {
458                        eval_args.push(
459                            self.evaluate_arena_ast(*arg_id, data_store, sheet_registry)?
460                                .into_literal(),
461                        );
462                    }
463                    return callable.invoke(self, &eval_args);
464                }
465
466                Err(ExcelError::new(ExcelErrorKind::Name)
467                    .with_message(format!("Unknown function: {name}")))
468            }
469        }
470    }
471
472    fn evaluate_ast_uncached(
473        &self,
474        node: &ASTNode,
475    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
476        // Plan-aware evaluation: build a plan for this node and execute accordingly.
477        // Provide the planner with a lightweight range-dimension probe and function lookup
478        // so it can select chunked reduction and arg-parallel strategies where appropriate.
479        let current_sheet = self.current_sheet.to_string();
480        let range_probe = |reference: &ReferenceType| -> Option<(u32, u32)> {
481            // Mirror Engine::resolve_range_storage bound normalization without materialising
482            use formualizer_parse::parser::ReferenceType as RT;
483            match reference {
484                RT::Range {
485                    sheet,
486                    start_row,
487                    start_col,
488                    end_row,
489                    end_col,
490                    ..
491                } => {
492                    let sheet_name = sheet.as_deref().unwrap_or(&current_sheet);
493                    // Start with provided values, fill None from used-region or sheet bounds.
494                    let mut sr = *start_row;
495                    let mut sc = *start_col;
496                    let mut er = *end_row;
497                    let mut ec = *end_col;
498
499                    // Column-only: rows are None on both ends
500                    if sr.is_none() && er.is_none() {
501                        // Full-column reference: anchor at row 1 for alignment across columns
502                        let scv = sc.unwrap_or(1);
503                        let ecv = ec.unwrap_or(scv);
504                        sr = Some(1);
505                        if let Some((_, max_r)) =
506                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
507                        {
508                            er = Some(max_r);
509                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
510                            er = Some(max_rows);
511                        }
512                    }
513
514                    // Row-only: cols are None on both ends
515                    if sc.is_none() && ec.is_none() {
516                        // Full-row reference: anchor at column 1 for alignment across rows
517                        let srv = sr.unwrap_or(1);
518                        let erv = er.unwrap_or(srv);
519                        sc = Some(1);
520                        if let Some((_, max_c)) =
521                            self.context.used_cols_for_rows(sheet_name, srv, erv)
522                        {
523                            ec = Some(max_c);
524                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
525                            ec = Some(max_cols);
526                        }
527                    }
528
529                    // Partially bounded (e.g., A1:A or A:A10)
530                    if sr.is_some() && er.is_none() {
531                        let scv = sc.unwrap_or(1);
532                        let ecv = ec.unwrap_or(scv);
533                        if let Some((_, max_r)) =
534                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
535                        {
536                            er = Some(max_r);
537                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
538                            er = Some(max_rows);
539                        }
540                    }
541                    if er.is_some() && sr.is_none() {
542                        // Open start: anchor at row 1
543                        sr = Some(1);
544                    }
545                    if sc.is_some() && ec.is_none() {
546                        let srv = sr.unwrap_or(1);
547                        let erv = er.unwrap_or(srv);
548                        if let Some((_, max_c)) =
549                            self.context.used_cols_for_rows(sheet_name, srv, erv)
550                        {
551                            ec = Some(max_c);
552                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
553                            ec = Some(max_cols);
554                        }
555                    }
556                    if ec.is_some() && sc.is_none() {
557                        // Open start: anchor at column 1
558                        sc = Some(1);
559                    }
560
561                    let sr = sr.unwrap_or(1);
562                    let sc = sc.unwrap_or(1);
563                    let er = er.unwrap_or(sr.saturating_sub(1));
564                    let ec = ec.unwrap_or(sc.saturating_sub(1));
565                    if er < sr || ec < sc {
566                        return Some((0, 0));
567                    }
568                    Some((er.saturating_sub(sr) + 1, ec.saturating_sub(sc) + 1))
569                }
570                RT::Cell { .. } => Some((1, 1)),
571                _ => None,
572            }
573        };
574        let fn_lookup = |ns: &str, name: &str| self.context.get_function(ns, name);
575
576        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
577            .with_range_probe(&range_probe)
578            .with_function_lookup(&fn_lookup);
579        let plan = planner.plan(node);
580        self.eval_with_plan(node, &plan.root)
581    }
582
583    fn eval_with_plan(
584        &self,
585        node: &ASTNode,
586        plan_node: &crate::planner::PlanNode,
587    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
588        match &node.node_type {
589            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
590            ASTNodeType::Reference { reference, .. } => {
591                if let Some(local) = self.resolve_local_reference(reference) {
592                    Ok(local)
593                } else {
594                    self.eval_reference_to_calc(reference)
595                }
596            }
597            ASTNodeType::UnaryOp { op, expr } => {
598                // For now, reuse existing unary implementation (which recurses).
599                // In a later phase, we can map plan_node.children[0].
600                self.eval_unary(op, expr)
601                    .map(crate::traits::CalcValue::Scalar)
602            }
603            ASTNodeType::BinaryOp { op, left, right } => self
604                .eval_binary(op, left, right)
605                .map(crate::traits::CalcValue::Scalar),
606            ASTNodeType::Function { name, args } => {
607                let strategy = plan_node.strategy;
608                if let Some(fun) = self.context.get_function("", name) {
609                    use crate::function::FnCaps;
610                    use crate::planner::ExecStrategy;
611                    let caps = fun.caps();
612
613                    // Short-circuit or volatile: always sequential
614                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
615                        return self.eval_function_to_calc(name, args);
616                    }
617
618                    // Windowed/chunked strategies are handled by the unified `eval()` path.
619
620                    // Arg-parallel: prewarm subexpressions and then dispatch
621                    if matches!(strategy, ExecStrategy::ArgParallel)
622                        && caps.contains(FnCaps::PARALLEL_ARGS)
623                    {
624                        // Sequential prewarm of subexpressions (safe without Sync bounds)
625                        for arg in args {
626                            match &arg.node_type {
627                                ASTNodeType::Reference { reference, .. } => {
628                                    let _ = self
629                                        .context
630                                        .resolve_range_view(reference, self.current_sheet);
631                                }
632                                _ => {
633                                    let _ = self.evaluate_ast(arg);
634                                }
635                            }
636                        }
637                        return self.eval_function_to_calc(name, args);
638                    }
639
640                    // Default path
641                    return self.eval_function_to_calc(name, args);
642                }
643                self.eval_function_to_calc(name, args)
644            }
645            ASTNodeType::Call { .. } => Err(ExcelError::new(ExcelErrorKind::NImpl)
646                .with_message("Immediate-invocation calls are not yet supported")),
647            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
648        }
649    }
650
651    /* ===================  reference  =================== */
652    fn eval_reference_to_calc(
653        &self,
654        reference: &ReferenceType,
655    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
656        let view = self
657            .context
658            .resolve_range_view(reference, self.current_sheet)?
659            .with_cancel_token(self.context.cancellation_token());
660
661        match reference {
662            ReferenceType::Cell { .. } => {
663                // For a single cell reference, just return the value.
664                Ok(crate::traits::CalcValue::Scalar(
665                    view.as_1x1().unwrap_or(LiteralValue::Empty),
666                ))
667            }
668            _ => Ok(crate::traits::CalcValue::Range(view)),
669        }
670    }
671
672    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
673        self.eval_reference_to_calc(reference)
674            .map(|cv| cv.into_literal())
675    }
676
677    /* ===================  unary ops  =================== */
678    fn eval_unary(&self, op: &str, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
679        if op == "@" {
680            if let ASTNodeType::Reference { reference, .. } = &expr.node_type {
681                return Ok(self.implicit_intersection_from_reference(reference));
682            }
683
684            let cv = self.evaluate_ast(expr)?;
685            return Ok(self.eval_implicit_intersection_calc(cv));
686        }
687
688        let v = self.evaluate_ast(expr)?.into_literal();
689        match v {
690            LiteralValue::Array(arr) => {
691                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
692            }
693            other => self.eval_unary_scalar(op, other),
694        }
695    }
696
697    fn eval_unary_scalar(&self, op: &str, v: LiteralValue) -> Result<LiteralValue, ExcelError> {
698        match op {
699            // Excel/LibreOffice treat unary `+` as a pass-through (identity) operator,
700            // not as a numeric coercion. `=+"2014F"` returns the text "2014F"; only the
701            // unary `-` form coerces operands to numbers. The `=+A1` idiom is common in
702            // finance models (Lotus 1-2-3 carry-over) and must preserve text labels.
703            "+" => Ok(v),
704            "-" => self.apply_number_unary(v, |n| -n),
705            "%" => self.apply_number_unary(v, |n| n / 100.0),
706            _ => {
707                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
708            }
709        }
710    }
711
712    fn eval_implicit_intersection_calc(&self, cv: crate::traits::CalcValue<'a>) -> LiteralValue {
713        let (cur_r0, cur_c0) = match self.current_cell {
714            Some(cell) => (cell.coord.row() as usize, cell.coord.col() as usize),
715            None => (0usize, 0usize),
716        };
717
718        match cv {
719            crate::traits::CalcValue::Scalar(v) => match v {
720                LiteralValue::Array(arr) => {
721                    if arr.is_empty() || arr.first().map(|r| r.is_empty()).unwrap_or(true) {
722                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
723                    }
724                    arr[0][0].clone()
725                }
726                other => other,
727            },
728            crate::traits::CalcValue::Range(rv) => {
729                if rv.is_empty() {
730                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
731                }
732
733                // Array results (array literals and many dynamic-array functions) are materialized
734                // into an owned RangeView with a temporary backing sheet ("__tmp").
735                // For explicit @, interpret these as anchored at the formula cell and select the
736                // top-left element.
737                if rv.sheet_name() == "__tmp" {
738                    return rv.get_cell(0, 0);
739                }
740
741                if let Some(v) = rv.as_1x1() {
742                    return v;
743                }
744
745                let (rows, cols) = rv.dims();
746                let sr = rv.start_row();
747                let sc = rv.start_col();
748                let er = rv.end_row();
749                let ec = rv.end_col();
750
751                // Excel-compatible implicit intersection (simplified):
752                // - Nx1: pick by row
753                // - 1xM: pick by column
754                // - NxM: pick by (row,col)
755                if cols == 1 {
756                    if cur_r0 < sr || cur_r0 > er {
757                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
758                    }
759                    let rel_r = cur_r0 - sr;
760                    return rv.get_cell(rel_r, 0);
761                }
762
763                if rows == 1 {
764                    if cur_c0 < sc || cur_c0 > ec {
765                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
766                    }
767                    let rel_c = cur_c0 - sc;
768                    return rv.get_cell(0, rel_c);
769                }
770
771                if cur_r0 < sr || cur_r0 > er || cur_c0 < sc || cur_c0 > ec {
772                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
773                }
774                let rel_r = cur_r0 - sr;
775                let rel_c = cur_c0 - sc;
776                rv.get_cell(rel_r, rel_c)
777            }
778            crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
779                ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
780            ),
781        }
782    }
783
784    fn implicit_intersection_from_reference(&self, reference: &ReferenceType) -> LiteralValue {
785        let (cur_r1, cur_c1) = match self.current_cell {
786            Some(cell) => (
787                cell.coord.row().saturating_add(1),
788                cell.coord.col().saturating_add(1),
789            ),
790            None => (1u32, 1u32),
791        };
792
793        match reference {
794            ReferenceType::Cell {
795                sheet, row, col, ..
796            } => {
797                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
798                match self
799                    .context
800                    .resolve_cell_reference(Some(sheet_name), *row, *col)
801                {
802                    Ok(v) => v,
803                    Err(e) => LiteralValue::Error(e),
804                }
805            }
806            ReferenceType::Range {
807                sheet,
808                start_row,
809                start_col,
810                end_row,
811                end_col,
812                ..
813            } => {
814                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
815
816                let (sr, sc, er, ec) = match (start_row, start_col, end_row, end_col) {
817                    (Some(sr), Some(sc), Some(er), Some(ec)) => (*sr, *sc, *er, *ec),
818                    _ => {
819                        // For open-ended/infinite ranges, fall back to the RangeView-based path.
820                        // This path may be less precise in minimal test contexts.
821                        let cv = match self.eval_reference_to_calc(reference) {
822                            Ok(cv) => cv,
823                            Err(e) => return LiteralValue::Error(e),
824                        };
825                        return self.eval_implicit_intersection_calc(cv);
826                    }
827                };
828
829                // Normalize bounds (A10:A1 is legal syntax; treat as swapped).
830                let (mut sr, mut er) = (sr, er);
831                let (mut sc, mut ec) = (sc, ec);
832                if sr > er {
833                    std::mem::swap(&mut sr, &mut er);
834                }
835                if sc > ec {
836                    std::mem::swap(&mut sc, &mut ec);
837                }
838
839                let pick = if sc == ec {
840                    // Column vector: intersect by row
841                    if cur_r1 < sr || cur_r1 > er {
842                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
843                    }
844                    (cur_r1, sc)
845                } else if sr == er {
846                    // Row vector: intersect by column
847                    if cur_c1 < sc || cur_c1 > ec {
848                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
849                    }
850                    (sr, cur_c1)
851                } else {
852                    // 2D: require both axes
853                    if cur_r1 < sr || cur_r1 > er || cur_c1 < sc || cur_c1 > ec {
854                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
855                    }
856                    (cur_r1, cur_c1)
857                };
858
859                match self
860                    .context
861                    .resolve_cell_reference(Some(sheet_name), pick.0, pick.1)
862                {
863                    Ok(v) => v,
864                    Err(e) => LiteralValue::Error(e),
865                }
866            }
867            // Named ranges / tables / external: fall back to materializing and intersecting.
868            other => {
869                let cv = match self.eval_reference_to_calc(other) {
870                    Ok(cv) => cv,
871                    Err(e) => return LiteralValue::Error(e),
872                };
873                self.eval_implicit_intersection_calc(cv)
874            }
875        }
876    }
877
878    fn apply_number_unary<F>(&self, v: LiteralValue, f: F) -> Result<LiteralValue, ExcelError>
879    where
880        F: Fn(f64) -> f64,
881    {
882        match crate::coercion::to_number_lenient_with_locale(&v, &self.context.locale()) {
883            Ok(n) => match crate::coercion::sanitize_numeric(f(n)) {
884                Ok(n2) => Ok(LiteralValue::Number(n2)),
885                Err(e) => Ok(LiteralValue::Error(e)),
886            },
887            Err(e) => Ok(LiteralValue::Error(e)),
888        }
889    }
890
891    /* ===================  binary ops  =================== */
892    fn eval_binary(
893        &self,
894        op: &str,
895        left: &ASTNode,
896        right: &ASTNode,
897    ) -> Result<LiteralValue, ExcelError> {
898        // Comparisons use dedicated path.
899        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
900            let l = self.evaluate_ast(left)?.into_literal();
901            let r = self.evaluate_ast(right)?.into_literal();
902            return self.compare(op, l, r);
903        }
904
905        let l_val = self.evaluate_ast(left)?.into_literal();
906        let r_val = self.evaluate_ast(right)?.into_literal();
907
908        match op {
909            "+" => self.add_sub_date_aware('+', l_val, r_val),
910            "-" => self.add_sub_date_aware('-', l_val, r_val),
911            "*" => self.numeric_binary(l_val, r_val, |a, b| a * b),
912            "/" => self.divide(l_val, r_val),
913            "^" => self.power(l_val, r_val),
914            "&" => Ok(LiteralValue::Text(format!(
915                "{}{}",
916                crate::coercion::to_text_invariant(&l_val),
917                crate::coercion::to_text_invariant(&r_val)
918            ))),
919            ":" => {
920                // Compute a combined reference; in value context return #REF! for now.
921                let lref = self.evaluate_ast_as_reference(left)?;
922                let rref = self.evaluate_ast_as_reference(right)?;
923                match crate::reference::combine_references(&lref, &rref) {
924                    Ok(_r) => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
925                        "Reference produced by ':' cannot be used directly as a value",
926                    )),
927                    Err(e) => Ok(LiteralValue::Error(e)),
928                }
929            }
930            _ => {
931                Err(ExcelError::new(ExcelErrorKind::NImpl)
932                    .with_message(format!("Binary op '{op}'")))
933            }
934        }
935    }
936
937    fn add_sub_date_aware(
938        &self,
939        op: char,
940        left: LiteralValue,
941        right: LiteralValue,
942    ) -> Result<LiteralValue, ExcelError> {
943        debug_assert!(op == '+' || op == '-');
944
945        self.broadcast_apply(left, right, |l, r| {
946            use LiteralValue::*;
947
948            let date_system = self.context.date_system();
949
950            let date_like_serial = |v: &LiteralValue| -> Option<f64> {
951                match v {
952                    Date(d) => Some(crate::builtins::datetime::date_to_serial_for(
953                        date_system,
954                        d,
955                    )),
956                    DateTime(dt) => Some(crate::builtins::datetime::datetime_to_serial_for(
957                        date_system,
958                        dt,
959                    )),
960                    _ => None,
961                }
962            };
963
964            let to_num = |v: &LiteralValue| -> Result<f64, ExcelError> {
965                crate::coercion::to_number_lenient_with_locale(v, &self.context.locale())
966            };
967
968            let serial_to_literal = |serial: f64| -> LiteralValue {
969                match crate::coercion::sanitize_numeric(serial) {
970                    Ok(serial) => {
971                        match crate::builtins::datetime::serial_to_datetime_for(date_system, serial)
972                        {
973                            Ok(dt) => {
974                                if dt.time() == chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
975                                    Date(dt.date())
976                                } else {
977                                    DateTime(dt)
978                                }
979                            }
980                            Err(e) => Error(e),
981                        }
982                    }
983                    Err(e) => Error(e),
984                }
985            };
986
987            // Date +/- number => date (propagate temporal tag)
988            if let Some(ls) = date_like_serial(&l) {
989                match op {
990                    '+' => {
991                        let rn = to_num(&r)?;
992                        return Ok(serial_to_literal(ls + rn));
993                    }
994                    '-' => {
995                        // Date - Date => numeric day delta (Excel-compatible)
996                        if let Some(rs) = date_like_serial(&r) {
997                            return Ok(Number(ls - rs));
998                        }
999                        let rn = to_num(&r)?;
1000                        return Ok(serial_to_literal(ls - rn));
1001                    }
1002                    _ => unreachable!(),
1003                }
1004            }
1005
1006            // Number + Date => date (commutative)
1007            if op == '+'
1008                && let Some(rs) = date_like_serial(&r)
1009            {
1010                let ln = to_num(&l)?;
1011                return Ok(serial_to_literal(ln + rs));
1012            }
1013
1014            // Fallback: regular numeric operation
1015            self.numeric_binary(l, r, |a, b| if op == '+' { a + b } else { a - b })
1016        })
1017    }
1018
1019    /* ===================  function calls  =================== */
1020    fn eval_function_to_calc(
1021        &self,
1022        name: &str,
1023        args: &[ASTNode],
1024    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1025        if let Some(fun) = self.context.get_function("", name) {
1026            let handles: Vec<ArgumentHandle> =
1027                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
1028            // Use the function's built-in dispatch method with a narrow FunctionContext
1029            let fctx = DefaultFunctionContext::new_with_sheet(
1030                self.context,
1031                self.current_cell,
1032                self.current_sheet,
1033            );
1034            return fun.dispatch(&handles, &fctx);
1035        }
1036
1037        if let Some(callable) = self.resolve_local_callable(name) {
1038            let mut eval_args = Vec::with_capacity(args.len());
1039            for arg in args {
1040                eval_args.push(self.evaluate_ast(arg)?.into_literal());
1041            }
1042            return callable.invoke(self, &eval_args);
1043        }
1044
1045        // Include the function name in the error message for better debugging
1046        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1047            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown function: {name}")),
1048        )))
1049    }
1050
1051    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
1052        self.eval_function_to_calc(name, args)
1053            .map(|cv| cv.into_literal())
1054    }
1055
1056    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
1057        DefaultFunctionContext::new_with_sheet(self.context, cell_ref.cloned(), self.current_sheet)
1058    }
1059
1060    /* ===================  array literal  =================== */
1061    fn eval_array_literal_to_calc(
1062        &self,
1063        rows: &[Vec<ASTNode>],
1064    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1065        let mut out = Vec::with_capacity(rows.len());
1066        for row in rows {
1067            let mut r = Vec::with_capacity(row.len());
1068            for cell in row {
1069                r.push(self.evaluate_ast(cell)?.into_literal());
1070            }
1071            out.push(r);
1072        }
1073        Ok(crate::traits::CalcValue::Range(
1074            crate::engine::range_view::RangeView::from_owned_rows(out, self.context.date_system()),
1075        ))
1076    }
1077
1078    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
1079        self.eval_array_literal_to_calc(rows)
1080            .map(|cv| cv.into_literal())
1081    }
1082
1083    /* ===================  helpers  =================== */
1084    fn numeric_binary<F>(
1085        &self,
1086        left: LiteralValue,
1087        right: LiteralValue,
1088        f: F,
1089    ) -> Result<LiteralValue, ExcelError>
1090    where
1091        F: Fn(f64, f64) -> f64 + Copy,
1092    {
1093        self.broadcast_apply(left, right, |l, r| {
1094            let a = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
1095            let b = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
1096            match (a, b) {
1097                (Ok(a), Ok(b)) => match crate::coercion::sanitize_numeric(f(a, b)) {
1098                    Ok(n2) => Ok(LiteralValue::Number(n2)),
1099                    Err(e) => Ok(LiteralValue::Error(e)),
1100                },
1101                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
1102            }
1103        })
1104    }
1105
1106    fn divide(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
1107        self.broadcast_apply(left, right, |l, r| {
1108            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
1109            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
1110            let (a, b) = match (ln, rn) {
1111                (Ok(a), Ok(b)) => (a, b),
1112                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
1113            };
1114            if b == 0.0 {
1115                return Ok(LiteralValue::Error(ExcelError::from_error_string(
1116                    "#DIV/0!",
1117                )));
1118            }
1119            match crate::coercion::sanitize_numeric(a / b) {
1120                Ok(n) => Ok(LiteralValue::Number(n)),
1121                Err(e) => Ok(LiteralValue::Error(e)),
1122            }
1123        })
1124    }
1125
1126    fn power(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
1127        self.broadcast_apply(left, right, |l, r| {
1128            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
1129            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
1130            let (a, b) = match (ln, rn) {
1131                (Ok(a), Ok(b)) => (a, b),
1132                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
1133            };
1134            // Excel domain: negative base with non-integer exponent -> #NUM!
1135            if a < 0.0 && b.fract() != 0.0 {
1136                return Ok(LiteralValue::Error(ExcelError::new_num()));
1137            }
1138            match crate::coercion::sanitize_numeric(a.powf(b)) {
1139                Ok(n) => Ok(LiteralValue::Number(n)),
1140                Err(e) => Ok(LiteralValue::Error(e)),
1141            }
1142        })
1143    }
1144
1145    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
1146    where
1147        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1148    {
1149        let mut out = Vec::with_capacity(arr.len());
1150        for row in arr {
1151            let mut new_row = Vec::with_capacity(row.len());
1152            for cell in row {
1153                new_row.push(match f(cell) {
1154                    Ok(v) => v,
1155                    Err(e) => LiteralValue::Error(e),
1156                });
1157            }
1158            out.push(new_row);
1159        }
1160        Ok(LiteralValue::Array(out))
1161    }
1162
1163    fn combine_arrays<F>(
1164        &self,
1165        l: Vec<Vec<LiteralValue>>,
1166        r: Vec<Vec<LiteralValue>>,
1167        f: F,
1168    ) -> Result<LiteralValue, ExcelError>
1169    where
1170        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1171    {
1172        // Use strict broadcasting across dimensions
1173        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
1174        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
1175        let target = match broadcast_shape(&[l_shape, r_shape]) {
1176            Ok(s) => s,
1177            Err(e) => return Ok(LiteralValue::Error(e)),
1178        };
1179
1180        let mut out = Vec::with_capacity(target.0);
1181        for i in 0..target.0 {
1182            let mut row = Vec::with_capacity(target.1);
1183            for j in 0..target.1 {
1184                let (li, lj) = project_index((i, j), l_shape);
1185                let (ri, rj) = project_index((i, j), r_shape);
1186                let lv = l
1187                    .get(li)
1188                    .and_then(|r| r.get(lj))
1189                    .cloned()
1190                    .unwrap_or(LiteralValue::Empty);
1191                let rv = r
1192                    .get(ri)
1193                    .and_then(|r| r.get(rj))
1194                    .cloned()
1195                    .unwrap_or(LiteralValue::Empty);
1196                row.push(match f(lv, rv) {
1197                    Ok(v) => v,
1198                    Err(e) => LiteralValue::Error(e),
1199                });
1200            }
1201            out.push(row);
1202        }
1203        Ok(LiteralValue::Array(out))
1204    }
1205
1206    fn broadcast_apply<F>(
1207        &self,
1208        left: LiteralValue,
1209        right: LiteralValue,
1210        f: F,
1211    ) -> Result<LiteralValue, ExcelError>
1212    where
1213        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1214    {
1215        use LiteralValue::*;
1216        match (left, right) {
1217            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
1218            (Array(arr), v) => {
1219                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1220                let shape_r = (1usize, 1usize);
1221                let target = match broadcast_shape(&[shape_l, shape_r]) {
1222                    Ok(s) => s,
1223                    Err(e) => return Ok(LiteralValue::Error(e)),
1224                };
1225                let mut out = Vec::with_capacity(target.0);
1226                for i in 0..target.0 {
1227                    let mut row = Vec::with_capacity(target.1);
1228                    for j in 0..target.1 {
1229                        let (li, lj) = project_index((i, j), shape_l);
1230                        let lv = arr
1231                            .get(li)
1232                            .and_then(|r| r.get(lj))
1233                            .cloned()
1234                            .unwrap_or(LiteralValue::Empty);
1235                        row.push(match f(lv, v.clone()) {
1236                            Ok(vv) => vv,
1237                            Err(e) => LiteralValue::Error(e),
1238                        });
1239                    }
1240                    out.push(row);
1241                }
1242                Ok(LiteralValue::Array(out))
1243            }
1244            (v, Array(arr)) => {
1245                let shape_l = (1usize, 1usize);
1246                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1247                let target = match broadcast_shape(&[shape_l, shape_r]) {
1248                    Ok(s) => s,
1249                    Err(e) => return Ok(LiteralValue::Error(e)),
1250                };
1251                let mut out = Vec::with_capacity(target.0);
1252                for i in 0..target.0 {
1253                    let mut row = Vec::with_capacity(target.1);
1254                    for j in 0..target.1 {
1255                        let (ri, rj) = project_index((i, j), shape_r);
1256                        let rv = arr
1257                            .get(ri)
1258                            .and_then(|r| r.get(rj))
1259                            .cloned()
1260                            .unwrap_or(LiteralValue::Empty);
1261                        row.push(match f(v.clone(), rv) {
1262                            Ok(vv) => vv,
1263                            Err(e) => LiteralValue::Error(e),
1264                        });
1265                    }
1266                    out.push(row);
1267                }
1268                Ok(LiteralValue::Array(out))
1269            }
1270            (l, r) => f(l, r),
1271        }
1272    }
1273
1274    /* ---------- coercion helpers ---------- */
1275    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
1276        coercion::to_number_lenient(v)
1277    }
1278
1279    fn coerce_text(&self, v: &LiteralValue) -> String {
1280        coercion::to_text_invariant(v)
1281    }
1282
1283    /* ---------- comparison ---------- */
1284    fn compare(
1285        &self,
1286        op: &str,
1287        left: LiteralValue,
1288        right: LiteralValue,
1289    ) -> Result<LiteralValue, ExcelError> {
1290        use LiteralValue::*;
1291        if matches!(left, Error(_)) {
1292            return Ok(left);
1293        }
1294        if matches!(right, Error(_)) {
1295            return Ok(right);
1296        }
1297
1298        // arrays: element‑wise with broadcasting
1299        match (left, right) {
1300            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
1301            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
1302            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
1303            (l, r) => {
1304                let res = match (l, r) {
1305                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
1306                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
1307                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
1308                    (Boolean(a), Boolean(b)) => {
1309                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
1310                    }
1311                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
1312                    (a, b) => {
1313                        // fallback to numeric coercion or text compare
1314                        let an = crate::coercion::to_number_lenient_with_locale(
1315                            &a,
1316                            &self.context.locale(),
1317                        )
1318                        .ok();
1319                        let bn = crate::coercion::to_number_lenient_with_locale(
1320                            &b,
1321                            &self.context.locale(),
1322                        )
1323                        .ok();
1324                        if let (Some(a), Some(b)) = (an, bn) {
1325                            self.cmp_f64(a, b, op)
1326                        } else {
1327                            self.cmp_text(
1328                                &crate::coercion::to_text_invariant(&a),
1329                                &crate::coercion::to_text_invariant(&b),
1330                                op,
1331                            )
1332                        }
1333                    }
1334                };
1335                Ok(LiteralValue::Boolean(res))
1336            }
1337        }
1338    }
1339
1340    fn cmp_f64(&self, a: f64, b: f64, op: &str) -> bool {
1341        match op {
1342            "=" => a == b,
1343            "<>" => a != b,
1344            ">" => a > b,
1345            "<" => a < b,
1346            ">=" => a >= b,
1347            "<=" => a <= b,
1348            _ => unreachable!(),
1349        }
1350    }
1351    fn cmp_text(&self, a: &str, b: &str, op: &str) -> bool {
1352        let loc = self.context.locale();
1353        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
1354        self.cmp_f64(
1355            a.cmp(&b) as i32 as f64,
1356            0.0,
1357            match op {
1358                "=" => "=",
1359                "<>" => "<>",
1360                ">" => ">",
1361                "<" => "<",
1362                ">=" => ">=",
1363                "<=" => "<=",
1364                _ => unreachable!(),
1365            },
1366        )
1367    }
1368}