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