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