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::{borrow::Cow, sync::Arc};
11
12use crate::engine::arena::ast::{CALL_NODE_NAME, SheetKey};
13use crate::engine::arena::{AstNodeData, AstNodeId, CompactRefType, DataStore};
14use crate::engine::sheet_registry::SheetRegistry;
15use crate::engine::template::canonical::LiteralSlotId;
16use crate::engine::used_extent::{
17    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
18};
19
20/// A spill reference whose operand is not a single-cell reference or name.
21fn spill_operand_error() -> ExcelError {
22    ExcelError::new(ExcelErrorKind::Ref)
23        .with_message("Spill reference operand must be a single cell or a name")
24}
25
26/// Postfix calls (`LAMBDA(x,x+1)(B1)`) are parsed and stored but not evaluated;
27/// the parsed tree and its arena copy fail the same way.
28fn call_expression_error() -> ExcelError {
29    ExcelError::new(ExcelErrorKind::NImpl)
30        .with_message("Immediate-invocation calls are not yet supported")
31}
32
33pub(crate) fn probe_range_dimensions<C: EvaluationContext + ?Sized>(
34    context: &C,
35    current_sheet: &str,
36    reference: &ReferenceType,
37) -> Option<(u32, u32)> {
38    match reference {
39        ReferenceType::Range {
40            sheet,
41            start_row,
42            start_col,
43            end_row,
44            end_col,
45            ..
46        } => {
47            let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
48            let extent = resolve_used_extent_with_fallback(
49                OpenRangeBounds {
50                    start_row: *start_row,
51                    start_column: *start_col,
52                    end_row: *end_row,
53                    end_column: *end_col,
54                },
55                ExtentPolicy::EvaluationCompat {
56                    fallback_row: None,
57                    fallback_column: None,
58                },
59                || context.sheet_bounds(sheet_name).map(|bounds| bounds.0),
60                || context.sheet_bounds(sheet_name).map(|bounds| bounds.1),
61                |first, last| context.used_rows_for_columns(sheet_name, first, last),
62                |first, last| context.used_cols_for_rows(sheet_name, first, last),
63            );
64            let Some(extent) = extent else {
65                return Some((0, 0));
66            };
67            Some((
68                extent.end_row - extent.start_row + 1,
69                extent.end_column - extent.start_column + 1,
70            ))
71        }
72        ReferenceType::Cell { .. } => Some((1, 1)),
73        _ => None,
74    }
75}
76
77#[derive(Clone)]
78pub enum LocalBinding {
79    Value(LiteralValue),
80    /// A local bound to a spreadsheet reference expression.
81    ///
82    /// `value` is exactly what [`LocalBinding::Value`] would have carried, so the
83    /// value path is unchanged; `reference` additionally lets a by-ref argument
84    /// slot see the range the local was bound to.
85    ValueWithReference {
86        value: LiteralValue,
87        reference: ReferenceType,
88    },
89    Callable(Arc<dyn crate::traits::CustomCallable>),
90}
91
92#[derive(Clone, Default)]
93pub struct LocalEnv {
94    head: Option<Arc<EnvFrame>>,
95}
96
97#[derive(Clone)]
98struct EnvFrame {
99    parent: Option<Arc<EnvFrame>>,
100    bindings: FxHashMap<String, LocalBinding>,
101}
102
103impl LocalEnv {
104    #[inline(always)]
105    pub fn is_empty(&self) -> bool {
106        self.head.is_none()
107    }
108
109    fn norm(name: &str) -> String {
110        name.to_ascii_uppercase()
111    }
112
113    pub fn lookup(&self, name: &str) -> Option<LocalBinding> {
114        self.head.as_ref()?;
115        let key = Self::norm(name);
116        let mut cur = self.head.as_ref().cloned();
117        while let Some(frame) = cur {
118            if let Some(v) = frame.bindings.get(&key) {
119                return Some(v.clone());
120            }
121            cur = frame.parent.clone();
122        }
123        None
124    }
125
126    pub fn with_binding(&self, name: &str, value: LocalBinding) -> Self {
127        let mut bindings = FxHashMap::default();
128        bindings.insert(Self::norm(name), value);
129        Self {
130            head: Some(Arc::new(EnvFrame {
131                parent: self.head.clone(),
132                bindings,
133            })),
134        }
135    }
136}
137
138#[derive(Clone, Copy)]
139pub(crate) struct InterpreterParameterBindings<'a> {
140    pub(crate) literal_slots_by_node: &'a FxHashMap<AstNodeId, LiteralSlotId>,
141    pub(crate) literal_values: &'a [LiteralValue],
142    /// Program 3: values of a family template's run-invariant calls,
143    /// computed once per run (the same cells for every member).
144    pub(crate) invariant_values: Option<&'a InvariantValues>,
145}
146
147/// Values of run-invariant calls by node (see `InterpreterParameterBindings`).
148pub(crate) type InvariantValues =
149    FxHashMap<AstNodeId, (LiteralValue, Option<crate::format::FormatId>)>;
150
151pub struct Interpreter<'a> {
152    pub context: &'a dyn EvaluationContext,
153    current_sheet: &'a str,
154    current_cell: Option<crate::CellRef>,
155    local_env: LocalEnv,
156    reference_row_delta: i64,
157    reference_col_delta: i64,
158    disable_ast_planner: bool,
159    parameter_bindings: Option<InterpreterParameterBindings<'a>>,
160}
161
162impl<'a> Interpreter<'a> {
163    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
164        Self {
165            context,
166            current_sheet,
167            current_cell: None,
168            local_env: LocalEnv::default(),
169            reference_row_delta: 0,
170            reference_col_delta: 0,
171            disable_ast_planner: false,
172            parameter_bindings: None,
173        }
174    }
175
176    pub fn new_with_cell(
177        context: &'a dyn EvaluationContext,
178        current_sheet: &'a str,
179        cell: crate::CellRef,
180    ) -> Self {
181        Self {
182            context,
183            current_sheet,
184            current_cell: Some(cell),
185            local_env: LocalEnv::default(),
186            reference_row_delta: 0,
187            reference_col_delta: 0,
188            disable_ast_planner: false,
189            parameter_bindings: None,
190        }
191    }
192
193    pub fn current_sheet(&self) -> &'a str {
194        self.current_sheet
195    }
196
197    pub fn local_env(&self) -> &LocalEnv {
198        &self.local_env
199    }
200
201    pub(crate) fn with_current_cell(&self, cell: crate::CellRef) -> Self {
202        Self {
203            context: self.context,
204            current_sheet: self.current_sheet,
205            current_cell: Some(cell),
206            local_env: self.local_env.clone(),
207            reference_row_delta: self.reference_row_delta,
208            reference_col_delta: self.reference_col_delta,
209            disable_ast_planner: self.disable_ast_planner,
210            parameter_bindings: self.parameter_bindings,
211        }
212    }
213
214    pub fn with_local_env(&self, env: LocalEnv) -> Self {
215        Self {
216            context: self.context,
217            current_sheet: self.current_sheet,
218            current_cell: self.current_cell,
219            local_env: env,
220            reference_row_delta: self.reference_row_delta,
221            reference_col_delta: self.reference_col_delta,
222            disable_ast_planner: self.disable_ast_planner,
223            parameter_bindings: self.parameter_bindings,
224        }
225    }
226
227    pub(crate) fn with_parameter_bindings(
228        &self,
229        bindings: InterpreterParameterBindings<'a>,
230    ) -> Self {
231        Self {
232            context: self.context,
233            current_sheet: self.current_sheet,
234            current_cell: self.current_cell,
235            local_env: self.local_env.clone(),
236            reference_row_delta: self.reference_row_delta,
237            reference_col_delta: self.reference_col_delta,
238            disable_ast_planner: self.disable_ast_planner,
239            parameter_bindings: Some(bindings),
240        }
241    }
242
243    fn effective_reference<'r>(
244        &self,
245        reference: &'r ReferenceType,
246    ) -> Result<Cow<'r, ReferenceType>, ExcelError> {
247        if self.reference_row_delta == 0 && self.reference_col_delta == 0 {
248            return Ok(Cow::Borrowed(reference));
249        }
250
251        Ok(Cow::Owned(relocate_reference_for_offset(
252            reference,
253            self.reference_row_delta,
254            self.reference_col_delta,
255        )?))
256    }
257
258    fn resolve_local_reference(
259        &self,
260        reference: &ReferenceType,
261    ) -> Option<crate::traits::CalcValue<'a>> {
262        if self.local_env.is_empty() {
263            return None;
264        }
265        let name = match reference {
266            ReferenceType::NamedRange(name) => name,
267            _ => return None,
268        };
269        match self.local_env.lookup(name)? {
270            LocalBinding::Value(v) => Some(crate::traits::CalcValue::Scalar(v)),
271            LocalBinding::ValueWithReference { value, .. } => {
272                Some(crate::traits::CalcValue::Scalar(value))
273            }
274            LocalBinding::Callable(c) => Some(crate::traits::CalcValue::Callable(c)),
275        }
276    }
277
278    fn resolve_local_callable(&self, name: &str) -> Option<Arc<dyn crate::traits::CustomCallable>> {
279        if self.local_env.is_empty() {
280            return None;
281        }
282        match self.local_env.lookup(name)? {
283            LocalBinding::Callable(c) => Some(c),
284            LocalBinding::Value(_) | LocalBinding::ValueWithReference { .. } => None,
285        }
286    }
287
288    pub fn resolve_local_name(&self, name: &str) -> Option<LocalBinding> {
289        self.local_env.lookup(name)
290    }
291
292    /// The spreadsheet reference a LET/LAMBDA local was bound to, when it was
293    /// bound to a reference expression rather than a computed value.
294    pub(crate) fn resolve_local_bound_reference(&self, name: &str) -> Option<ReferenceType> {
295        if self.local_env.is_empty() {
296            return None;
297        }
298        match self.local_env.lookup(name)? {
299            LocalBinding::ValueWithReference { reference, .. } => Some(reference),
300            LocalBinding::Value(_) | LocalBinding::Callable(_) => None,
301        }
302    }
303
304    pub fn resolve_range_view<'c>(
305        &'c self,
306        reference: &ReferenceType,
307        current_sheet: &str,
308    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
309        self.context.resolve_range_view(reference, current_sheet)
310    }
311
312    /// Evaluate an AST node in a reference context and return a ReferenceType.
313    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
314    /// and spill planning. Functions that can return references must set
315    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
316    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
317        match &node.node_type {
318            ASTNodeType::Reference { reference, .. } => {
319                self.reference_for_current_offset(reference)
320            }
321            ASTNodeType::Function { name, args } => {
322                if let Some(fun) = self.context.get_function("", name) {
323                    // Build handles; allow function to decide reference semantics
324                    let handles: Vec<ArgumentHandle> =
325                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
326                    let fctx = DefaultFunctionContext::new_with_sheet(
327                        self.context,
328                        None,
329                        self.current_sheet,
330                    );
331                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
332                        res
333                    } else {
334                        Err(ExcelError::new(ExcelErrorKind::Ref)
335                            .with_message("Function does not return a reference"))
336                    }
337                } else {
338                    Err(ExcelError::new(ExcelErrorKind::Name)
339                        .with_message(format!("Unknown function: {name}")))
340                }
341            }
342            ASTNodeType::BinaryOp { op, left, right } if op == ":" => {
343                let lref = self.evaluate_ast_as_reference(left)?;
344                let rref = self.evaluate_ast_as_reference(right)?;
345                crate::reference::combine_references(&lref, &rref)
346            }
347            ASTNodeType::UnaryOp { op, expr } if op == "#" => self.ast_spill_reference(expr),
348            ASTNodeType::Array(_)
349            | ASTNodeType::UnaryOp { .. }
350            | ASTNodeType::BinaryOp { .. }
351            | ASTNodeType::Call { .. }
352            | ASTNodeType::Literal(_)
353            | ASTNodeType::Omitted => Err(ExcelError::new(ExcelErrorKind::Ref)
354                .with_message("Expression cannot be used as a reference")),
355        }
356    }
357
358    pub(crate) fn try_evaluate_ast_as_reference(
359        &self,
360        node: &ASTNode,
361    ) -> Option<Result<ReferenceType, ExcelError>> {
362        let ASTNodeType::Function { name, args } = &node.node_type else {
363            return Some(self.evaluate_ast_as_reference(node));
364        };
365        let fun = match self.context.get_function("", name) {
366            Some(fun) => fun,
367            None => {
368                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
369                    .with_message(format!("Unknown function: {name}"))));
370            }
371        };
372        let handles: Vec<ArgumentHandle> = args
373            .iter()
374            .map(|arg| ArgumentHandle::new(arg, self))
375            .collect();
376        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
377        fun.eval_reference(&handles, &fctx)
378    }
379
380    pub(crate) fn evaluate_arena_ast_as_reference(
381        &self,
382        node_id: AstNodeId,
383        data_store: &DataStore,
384        sheet_registry: &SheetRegistry,
385    ) -> Result<ReferenceType, ExcelError> {
386        let node = data_store.get_node(node_id).ok_or_else(|| {
387            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
388        })?;
389
390        match node {
391            AstNodeData::Reference { ref_type, .. } => {
392                let reference =
393                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
394                self.reference_for_current_offset(&reference)
395            }
396            AstNodeData::Function { name_id, .. } => {
397                let name = data_store.resolve_ast_string(*name_id);
398                if name == CALL_NODE_NAME {
399                    return Err(call_expression_error());
400                }
401                let fun = self.context.get_function("", name).ok_or_else(|| {
402                    ExcelError::new(ExcelErrorKind::Name)
403                        .with_message(format!("Unknown function: {name}"))
404                })?;
405
406                let args = data_store.get_args(node_id).ok_or_else(|| {
407                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
408                })?;
409
410                let handles: Vec<ArgumentHandle> = args
411                    .iter()
412                    .copied()
413                    .map(|arg_id| {
414                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
415                    })
416                    .collect();
417
418                let fctx =
419                    DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
420
421                fun.eval_reference(&handles, &fctx).ok_or_else(|| {
422                    ExcelError::new(ExcelErrorKind::Ref)
423                        .with_message("Function does not return a reference")
424                })?
425            }
426            AstNodeData::BinaryOp {
427                op_id,
428                left_id,
429                right_id,
430            } => {
431                let op = data_store.resolve_ast_string(*op_id);
432                if op != ":" {
433                    return Err(ExcelError::new(ExcelErrorKind::Ref)
434                        .with_message("Expression cannot be used as a reference"));
435                }
436                let lref =
437                    self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
438                let rref =
439                    self.evaluate_arena_ast_as_reference(*right_id, data_store, sheet_registry)?;
440                crate::reference::combine_references(&lref, &rref)
441            }
442            AstNodeData::UnaryOp { op_id, expr_id }
443                if data_store.resolve_ast_string(*op_id) == "#" =>
444            {
445                self.arena_spill_reference(*expr_id, data_store, sheet_registry)
446            }
447            _ => Err(ExcelError::new(ExcelErrorKind::Ref)
448                .with_message("Expression cannot be used as a reference")),
449        }
450    }
451
452    /// The anchor a spill operand names: a single-cell reference or a name,
453    /// including a LET/LAMBDA local bound to one. Anything else is `#REF!`.
454    fn spill_anchor_operand(&self, reference: &ReferenceType) -> Result<ReferenceType, ExcelError> {
455        let anchor = match reference {
456            ReferenceType::NamedRange(name) if self.resolve_local_name(name).is_some() => self
457                .resolve_local_bound_reference(name)
458                .ok_or_else(spill_operand_error)?,
459            _ => self
460                .reference_for_current_offset(reference)
461                .map_err(|_| spill_operand_error())?,
462        };
463        match anchor {
464            ReferenceType::Cell { .. } | ReferenceType::NamedRange(_) => Ok(anchor),
465            _ => Err(spill_operand_error()),
466        }
467    }
468
469    /// Resolve a spill reference (`A1#`, `ANCHORARRAY(A1)`) whose operand is
470    /// the written `reference`, to the anchor's current spill rectangle.
471    pub(crate) fn resolve_spill_reference(
472        &self,
473        reference: &ReferenceType,
474    ) -> Result<ReferenceType, ExcelError> {
475        let anchor = self.spill_anchor_operand(reference)?;
476        self.context
477            .resolve_spill_reference(&anchor, self.current_sheet)
478    }
479
480    /// The `#` operator on an AST operand: only a written reference qualifies.
481    pub(crate) fn ast_spill_reference(
482        &self,
483        operand: &ASTNode,
484    ) -> Result<ReferenceType, ExcelError> {
485        match &operand.node_type {
486            ASTNodeType::Reference { reference, .. } => self.resolve_spill_reference(reference),
487            _ => Err(spill_operand_error()),
488        }
489    }
490
491    /// The `#` operator on an arena operand: only a written reference qualifies.
492    pub(crate) fn arena_spill_reference(
493        &self,
494        operand: AstNodeId,
495        data_store: &DataStore,
496        sheet_registry: &SheetRegistry,
497    ) -> Result<ReferenceType, ExcelError> {
498        match data_store.get_node(operand) {
499            Some(AstNodeData::Reference { ref_type, .. }) => {
500                let reference =
501                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
502                self.resolve_spill_reference(&reference)
503            }
504            _ => Err(spill_operand_error()),
505        }
506    }
507
508    pub(crate) fn try_evaluate_arena_ast_as_reference(
509        &self,
510        node_id: AstNodeId,
511        data_store: &DataStore,
512        sheet_registry: &SheetRegistry,
513    ) -> Option<Result<ReferenceType, ExcelError>> {
514        let node = match data_store.get_node(node_id) {
515            Some(node) => node,
516            None => {
517                return Some(Err(
518                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
519                ));
520            }
521        };
522        let AstNodeData::Function { name_id, .. } = node else {
523            return Some(self.evaluate_arena_ast_as_reference(node_id, data_store, sheet_registry));
524        };
525        let name = data_store.resolve_ast_string(*name_id);
526        if name == CALL_NODE_NAME {
527            return Some(Err(call_expression_error()));
528        }
529        let fun = match self.context.get_function("", name) {
530            Some(fun) => fun,
531            None => {
532                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
533                    .with_message(format!("Unknown function: {name}"))));
534            }
535        };
536        let args = match data_store.get_args(node_id) {
537            Some(args) => args,
538            None => {
539                return Some(Err(
540                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
541                ));
542            }
543        };
544        let handles: Vec<ArgumentHandle> = args
545            .iter()
546            .copied()
547            .map(|arg_id| ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry))
548            .collect();
549        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
550        fun.eval_reference(&handles, &fctx)
551    }
552
553    /* ===================  public  =================== */
554    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
555        self.evaluate_ast_uncached(node)
556    }
557
558    pub(crate) fn evaluate_ast_with_offset(
559        &self,
560        node: &ASTNode,
561        row_delta: i64,
562        col_delta: i64,
563    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
564        let offset = Self {
565            context: self.context,
566            current_sheet: self.current_sheet,
567            current_cell: self.current_cell,
568            local_env: self.local_env.clone(),
569            reference_row_delta: row_delta,
570            reference_col_delta: col_delta,
571            disable_ast_planner: true,
572            parameter_bindings: self.parameter_bindings,
573        };
574        offset.evaluate_ast_uncached(node)
575    }
576
577    pub(crate) fn reference_for_current_offset(
578        &self,
579        reference: &ReferenceType,
580    ) -> Result<ReferenceType, ExcelError> {
581        self.effective_reference(reference)
582            .map(|reference| reference.into_owned())
583    }
584
585    /// Evaluate a formula given as a template plus offset (a compressed
586    /// family member, or a formula on its own cell at offset zero).
587    pub(crate) fn evaluate_formula_view(
588        &self,
589        view: crate::engine::graph::FormulaView,
590        data_store: &DataStore,
591        sheet_registry: &SheetRegistry,
592    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
593        if view.row_delta == 0 && view.col_delta == 0 {
594            self.evaluate_arena_ast(view.template, data_store, sheet_registry)
595        } else {
596            self.evaluate_arena_ast_with_offset(
597                view.template,
598                view.row_delta,
599                view.col_delta,
600                data_store,
601                sheet_registry,
602            )
603        }
604    }
605
606    pub(crate) fn evaluate_arena_ast_with_offset(
607        &self,
608        node_id: AstNodeId,
609        row_delta: i64,
610        col_delta: i64,
611        data_store: &DataStore,
612        sheet_registry: &SheetRegistry,
613    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
614        let offset = Self {
615            context: self.context,
616            current_sheet: self.current_sheet,
617            current_cell: self.current_cell,
618            local_env: self.local_env.clone(),
619            reference_row_delta: row_delta,
620            reference_col_delta: col_delta,
621            disable_ast_planner: true,
622            parameter_bindings: self.parameter_bindings,
623        };
624        offset.evaluate_arena_ast(node_id, data_store, sheet_registry)
625    }
626
627    #[inline]
628    fn annotated(
629        value: LiteralValue,
630        format: Option<crate::format::FormatId>,
631    ) -> crate::traits::CalcValue<'a> {
632        match format {
633            Some(format) => crate::traits::CalcValue::AnnotatedScalar(value, format),
634            None => crate::traits::CalcValue::Scalar(value),
635        }
636    }
637
638    fn annotate_cell_value(
639        &self,
640        sheet: Option<&str>,
641        row: u32,
642        col: u32,
643        value: LiteralValue,
644    ) -> crate::traits::CalcValue<'a> {
645        match self
646            .context
647            .resolve_cell_format(sheet, row, col, self.current_sheet)
648        {
649            Some(format) => crate::traits::CalcValue::AnnotatedScalar(value, format),
650            None => crate::traits::CalcValue::Scalar(value),
651        }
652    }
653
654    fn binary_format(
655        &self,
656        op: char,
657        left: Option<crate::format::FormatId>,
658        right: Option<crate::format::FormatId>,
659    ) -> Option<crate::format::FormatId> {
660        use formualizer_common::numfmt::FormatClass;
661        let class =
662            |id: Option<crate::format::FormatId>| id.and_then(|id| self.context.format_class(id));
663        let left = class(left);
664        let right = class(right);
665        let is_plain = |class: &Option<FormatClass>| {
666            matches!(
667                class,
668                None | Some(FormatClass::General | FormatClass::Number { .. })
669            )
670        };
671        // This table is intentionally closed. LibreOffice measurement establishes
672        // Date+Time and Date+Percent; unlisted pairs (including Date+Date,
673        // Duration+Date, Date+Currency, DateTime+Time, and Date+Text) drop the
674        // annotation rather than guessing a display class.
675        match (op, left.as_ref(), right.as_ref()) {
676            ('+', Some(FormatClass::Date), Some(FormatClass::Time))
677            | ('+', Some(FormatClass::Time), Some(FormatClass::Date)) => {
678                Some(crate::format::FormatId::DATETIME)
679            }
680            ('+', Some(FormatClass::Date), Some(FormatClass::Percent { .. }))
681            | ('+', Some(FormatClass::Percent { .. }), Some(FormatClass::Date)) => {
682                Some(crate::format::FormatId::DATE)
683            }
684            ('+' | '-', Some(FormatClass::Date), r) if is_plain(&r.cloned()) => {
685                Some(crate::format::FormatId::DATE)
686            }
687            ('+', l, Some(FormatClass::Date)) if is_plain(&l.cloned()) => {
688                Some(crate::format::FormatId::DATE)
689            }
690            ('+' | '-', Some(FormatClass::Time), r) if is_plain(&r.cloned()) => {
691                Some(crate::format::FormatId::TIME)
692            }
693            ('+', l, Some(FormatClass::Time)) if is_plain(&l.cloned()) => {
694                Some(crate::format::FormatId::TIME)
695            }
696            ('+' | '-', Some(FormatClass::DateTime), r) if is_plain(&r.cloned()) => {
697                Some(crate::format::FormatId::DATETIME)
698            }
699            ('+', l, Some(FormatClass::DateTime)) if is_plain(&l.cloned()) => {
700                Some(crate::format::FormatId::DATETIME)
701            }
702            ('+' | '-', Some(FormatClass::Duration), r) if is_plain(&r.cloned()) => {
703                Some(crate::format::FormatId::DURATION)
704            }
705            ('+', l, Some(FormatClass::Duration)) if is_plain(&l.cloned()) => {
706                Some(crate::format::FormatId::DURATION)
707            }
708            _ => None,
709        }
710    }
711
712    fn annotate_numeric_result(
713        &self,
714        value: LiteralValue,
715        format: Option<crate::format::FormatId>,
716    ) -> crate::traits::CalcValue<'a> {
717        match (value, format) {
718            (value @ LiteralValue::Number(_), Some(format)) => {
719                crate::traits::CalcValue::AnnotatedScalar(value, format)
720            }
721            (value, _) => crate::traits::CalcValue::Scalar(value),
722        }
723    }
724
725    /// A unary operator other than `@` on an evaluated operand (shared by
726    /// the AST walk and the elementwise lift).
727    pub(crate) fn apply_unary_op(
728        &self,
729        op: &str,
730        expr: crate::traits::CalcValue<'a>,
731    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
732        // For now, materialize for operators. Future: virtual range ops.
733        let v = expr.into_literal();
734        match v {
735            LiteralValue::Array(arr) => self
736                .map_array(arr, |cell| self.eval_unary_scalar(op, cell))
737                .map(crate::traits::CalcValue::Scalar),
738            other => self
739                .eval_unary_scalar(op, other)
740                .map(crate::traits::CalcValue::Scalar),
741        }
742    }
743
744    /// A binary operator other than `:` on evaluated operands and their
745    /// format annotations (shared by the AST walk and the elementwise lift).
746    pub(crate) fn apply_binary_op(
747        &self,
748        op: &str,
749        left: LiteralValue,
750        left_format: Option<crate::format::FormatId>,
751        right: LiteralValue,
752        right_format: Option<crate::format::FormatId>,
753    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
754        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
755            return self
756                .compare(op, left, right)
757                .map(crate::traits::CalcValue::Scalar);
758        }
759
760        match op {
761            "+" => self.numeric_binary(left, right, b'+').map(|value| {
762                self.annotate_numeric_result(
763                    value,
764                    self.binary_format('+', left_format, right_format),
765                )
766            }),
767            "-" => self.numeric_binary(left, right, b'-').map(|value| {
768                self.annotate_numeric_result(
769                    value,
770                    self.binary_format('-', left_format, right_format),
771                )
772            }),
773            "*" => self
774                .numeric_binary(left, right, b'*')
775                .map(crate::traits::CalcValue::Scalar),
776            "/" => self
777                .numeric_binary(left, right, b'/')
778                .map(crate::traits::CalcValue::Scalar),
779            "^" => self
780                .numeric_binary(left, right, b'^')
781                .map(crate::traits::CalcValue::Scalar),
782            "&" => self
783                .concat_values(left, right)
784                .map(crate::traits::CalcValue::Scalar),
785            _ => {
786                Err(ExcelError::new(ExcelErrorKind::NImpl)
787                    .with_message(format!("Binary op '{op}'")))
788            }
789        }
790    }
791
792    pub(crate) fn evaluate_arena_ast(
793        &self,
794        node_id: AstNodeId,
795        data_store: &DataStore,
796        sheet_registry: &SheetRegistry,
797    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
798        let node = data_store.get_node(node_id).ok_or_else(|| {
799            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
800        })?;
801
802        match node {
803            AstNodeData::Literal(vref) => {
804                if let Some(bindings) = self.parameter_bindings
805                    && let Some(slot_id) = bindings.literal_slots_by_node.get(&node_id)
806                    && let Some(value) = bindings.literal_values.get(slot_id.0 as usize)
807                {
808                    return Ok(crate::traits::CalcValue::Scalar(value.clone()));
809                }
810                Ok(crate::traits::CalcValue::Scalar(
811                    data_store.retrieve_value(*vref),
812                ))
813            }
814            AstNodeData::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
815            AstNodeData::Reference { ref_type, .. } => {
816                if self.local_env.is_empty()
817                    && let CompactRefType::Cell {
818                        sheet,
819                        row,
820                        col,
821                        row_abs,
822                        col_abs,
823                    } = ref_type
824                    && *row > 0
825                    && *col > 0
826                {
827                    let sheet_name = match sheet {
828                        Some(SheetKey::Id(id)) => Some(sheet_registry.name(*id)),
829                        Some(SheetKey::Name(name_id)) => {
830                            Some(data_store.resolve_ast_string(*name_id))
831                        }
832                        None => None,
833                    };
834                    let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
835                    let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
836                    let (value, format) = self.context.resolve_cell_reference_value_formatted(
837                        sheet_name,
838                        row,
839                        col,
840                        self.current_sheet,
841                    )?;
842                    Ok(Self::annotated(value, format))
843                } else {
844                    let reference =
845                        data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
846                    let reference = self.effective_reference(&reference)?;
847                    if let Some(local) = self.resolve_local_reference(&reference) {
848                        return Ok(local);
849                    }
850                    self.eval_reference_to_calc(&reference)
851                }
852            }
853            AstNodeData::UnaryOp { op_id, expr_id } => {
854                let op = data_store.resolve_ast_string(*op_id);
855                if op == "#" {
856                    let reference =
857                        self.arena_spill_reference(*expr_id, data_store, sheet_registry)?;
858                    return self.eval_reference_to_calc(&reference);
859                }
860                if op == "@" {
861                    // Prefer reference-aware implicit intersection so we don't depend on
862                    // RangeView absolute coordinates (important for lightweight test contexts).
863                    // The reference is placed at this cell (a shared family's
864                    // arena reference is in its origin frame), as on the
865                    // plain-reference path above.
866                    if let Some(AstNodeData::Reference { ref_type, .. }) =
867                        data_store.get_node(*expr_id)
868                    {
869                        let reference = data_store
870                            .reconstruct_reference_type_for_eval(ref_type, sheet_registry);
871                        let v = match self.effective_reference(&reference) {
872                            Ok(reference) => self.implicit_intersection_from_reference(&reference),
873                            Err(e) => LiteralValue::Error(e),
874                        };
875                        return Ok(crate::traits::CalcValue::Scalar(v));
876                    }
877
878                    let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
879                    let v = self.eval_implicit_intersection_calc(expr);
880                    return Ok(crate::traits::CalcValue::Scalar(v));
881                }
882                let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
883                self.apply_unary_op(op, expr)
884            }
885            AstNodeData::BinaryOp {
886                op_id,
887                left_id,
888                right_id,
889            } => {
890                let op = data_store.resolve_ast_string(*op_id);
891                if op == ":" {
892                    let lref =
893                        self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
894                    let rref = self.evaluate_arena_ast_as_reference(
895                        *right_id,
896                        data_store,
897                        sheet_registry,
898                    )?;
899                    return match crate::reference::combine_references(&lref, &rref) {
900                        Ok(_r) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
901                            ExcelError::new(ExcelErrorKind::Ref).with_message(
902                                "Reference produced by ':' cannot be used directly as a value",
903                            ),
904                        ))),
905                        Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
906                    };
907                }
908
909                let left_calc = self.evaluate_arena_ast(*left_id, data_store, sheet_registry)?;
910                let left_format = left_calc.format_id();
911                let left = left_calc.into_literal();
912                let right_calc = self.evaluate_arena_ast(*right_id, data_store, sheet_registry)?;
913                let right_format = right_calc.format_id();
914                let right = right_calc.into_literal();
915                self.apply_binary_op(op, left, left_format, right, right_format)
916            }
917            AstNodeData::Array { .. } => {
918                let (rows, cols, elements) =
919                    data_store.get_array_elems(node_id).ok_or_else(|| {
920                        ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
921                    })?;
922
923                let rows_usize = rows as usize;
924                let cols_usize = cols as usize;
925                let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows_usize);
926                for r in 0..rows_usize {
927                    let mut row = Vec::with_capacity(cols_usize);
928                    for c in 0..cols_usize {
929                        let idx = r * cols_usize + c;
930                        if let Some(&elem_id) = elements.get(idx) {
931                            row.push(
932                                self.evaluate_arena_ast(elem_id, data_store, sheet_registry)?
933                                    .into_literal(),
934                            );
935                        }
936                    }
937                    out.push(row);
938                }
939
940                Ok(crate::traits::CalcValue::Range(
941                    crate::engine::range_view::RangeView::from_owned_rows(
942                        out,
943                        self.context.date_system(),
944                    ),
945                ))
946            }
947            AstNodeData::Function { name_id, .. } => {
948                if let Some(bindings) = self.parameter_bindings
949                    && let Some(values) = bindings.invariant_values
950                    && let Some((value, format)) = values.get(&node_id)
951                {
952                    return Ok(Self::annotated(value.clone(), *format));
953                }
954                let name = data_store.resolve_ast_string(*name_id);
955                if name == CALL_NODE_NAME {
956                    return Err(call_expression_error());
957                }
958                let args = data_store.get_args(node_id).ok_or_else(|| {
959                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
960                })?;
961
962                if let Some(fun) = self.context.get_function("", name) {
963                    let handles: Vec<ArgumentHandle> = args
964                        .iter()
965                        .copied()
966                        .map(|arg_id| {
967                            ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
968                        })
969                        .collect();
970
971                    let fctx = DefaultFunctionContext::new_with_sheet(
972                        self.context,
973                        self.current_cell,
974                        self.current_sheet,
975                    );
976
977                    return fun.dispatch(&handles, &fctx);
978                }
979
980                if let Some(callable) = self.resolve_local_callable(name) {
981                    let mut eval_args = Vec::with_capacity(args.len());
982                    let mut arg_references = Vec::with_capacity(args.len());
983                    for arg_id in args {
984                        // Same rule as the AST twin above.
985                        arg_references.push(
986                            ArgumentHandle::new_arena(*arg_id, self, data_store, sheet_registry)
987                                .bound_reference_in_env(&self.local_env),
988                        );
989                        eval_args.push(
990                            self.evaluate_arena_ast(*arg_id, data_store, sheet_registry)?
991                                .into_literal(),
992                        );
993                    }
994                    return callable.invoke_with_references(self, &eval_args, &arg_references);
995                }
996
997                Err(ExcelError::new(ExcelErrorKind::Name)
998                    .with_message(format!("Unknown function: {name}")))
999            }
1000        }
1001    }
1002
1003    fn evaluate_ast_uncached(
1004        &self,
1005        node: &ASTNode,
1006    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1007        if self.disable_ast_planner {
1008            return self.eval_tree_uncached(node);
1009        }
1010
1011        // Plan-aware evaluation: build a plan for this node and execute accordingly.
1012        // Provide the planner with a lightweight range-dimension probe and function lookup
1013        // so it can select chunked reduction and arg-parallel strategies where appropriate.
1014        let current_sheet = self.current_sheet.to_string();
1015        let range_probe = |reference: &ReferenceType| {
1016            probe_range_dimensions(self.context, &current_sheet, reference)
1017        };
1018        let fn_lookup = |ns: &str, name: &str| self.context.get_function(ns, name);
1019
1020        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
1021            .with_range_probe(&range_probe)
1022            .with_function_lookup(&fn_lookup);
1023        let plan = planner.plan(node);
1024        self.eval_with_plan(node, &plan.root)
1025    }
1026
1027    fn eval_tree_uncached(
1028        &self,
1029        node: &ASTNode,
1030    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1031        match &node.node_type {
1032            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
1033            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
1034            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
1035            ASTNodeType::UnaryOp { op, expr } if op == "#" => {
1036                self.eval_reference_to_calc(&self.ast_spill_reference(expr)?)
1037            }
1038            ASTNodeType::UnaryOp { op, expr } => self
1039                .eval_unary(op, expr)
1040                .map(crate::traits::CalcValue::Scalar),
1041            ASTNodeType::BinaryOp { op, left, right } => self.eval_binary(op, left, right),
1042            ASTNodeType::Function { name, args } => self.eval_function_to_calc(name, args),
1043            ASTNodeType::Call { .. } => Err(call_expression_error()),
1044            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
1045        }
1046    }
1047
1048    fn eval_with_plan(
1049        &self,
1050        node: &ASTNode,
1051        plan_node: &crate::planner::PlanNode,
1052    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1053        match &node.node_type {
1054            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
1055            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
1056            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
1057            ASTNodeType::UnaryOp { op, expr } if op == "#" => {
1058                self.eval_reference_to_calc(&self.ast_spill_reference(expr)?)
1059            }
1060            ASTNodeType::UnaryOp { op, expr } => {
1061                // For now, reuse existing unary implementation (which recurses).
1062                // In a later phase, we can map plan_node.children[0].
1063                self.eval_unary(op, expr)
1064                    .map(crate::traits::CalcValue::Scalar)
1065            }
1066            ASTNodeType::BinaryOp { op, left, right } => self.eval_binary(op, left, right),
1067            ASTNodeType::Function { name, args } => {
1068                let strategy = plan_node.strategy;
1069                if let Some(fun) = self.context.get_function("", name) {
1070                    use crate::function::FnCaps;
1071                    use crate::planner::ExecStrategy;
1072                    let caps = fun.caps();
1073
1074                    // Short-circuit or volatile: always sequential
1075                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
1076                        return self.eval_function_to_calc(name, args);
1077                    }
1078
1079                    // Windowed/chunked strategies are handled by the unified `eval()` path.
1080
1081                    // Arg-parallel: prewarm subexpressions and then dispatch
1082                    if matches!(strategy, ExecStrategy::ArgParallel)
1083                        && caps.contains(FnCaps::PARALLEL_ARGS)
1084                    {
1085                        // Sequential prewarm of subexpressions (safe without Sync bounds)
1086                        for arg in args {
1087                            match &arg.node_type {
1088                                ASTNodeType::Reference { reference, .. } => {
1089                                    if let Ok(reference) = self.effective_reference(reference) {
1090                                        let _ = self
1091                                            .context
1092                                            .resolve_range_view(&reference, self.current_sheet);
1093                                    }
1094                                }
1095                                _ => {
1096                                    let _ = self.evaluate_ast(arg);
1097                                }
1098                            }
1099                        }
1100                        return self.eval_function_to_calc(name, args);
1101                    }
1102
1103                    // Default path
1104                    return self.eval_function_to_calc(name, args);
1105                }
1106                self.eval_function_to_calc(name, args)
1107            }
1108            ASTNodeType::Call { .. } => Err(call_expression_error()),
1109            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
1110        }
1111    }
1112
1113    /* ===================  reference  =================== */
1114    fn eval_ast_reference_to_calc(
1115        &self,
1116        reference: &ReferenceType,
1117    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1118        if !self.local_env.is_empty() {
1119            let reference = self.effective_reference(reference)?;
1120            if let Some(local) = self.resolve_local_reference(&reference) {
1121                return Ok(local);
1122            }
1123            return self.eval_reference_to_calc(&reference);
1124        }
1125
1126        if let ReferenceType::Cell {
1127            sheet,
1128            row,
1129            col,
1130            row_abs,
1131            col_abs,
1132        } = reference
1133        {
1134            let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
1135            let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
1136            let (value, format) = self.context.resolve_cell_reference_value_formatted(
1137                sheet.as_deref(),
1138                row,
1139                col,
1140                self.current_sheet,
1141            )?;
1142            return Ok(Self::annotated(value, format));
1143        }
1144
1145        let reference = self.effective_reference(reference)?;
1146        self.eval_reference_to_calc(&reference)
1147    }
1148
1149    fn eval_reference_to_calc(
1150        &self,
1151        reference: &ReferenceType,
1152    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1153        if let ReferenceType::Cell {
1154            sheet, row, col, ..
1155        } = reference
1156        {
1157            let value = self.context.resolve_cell_reference_value(
1158                sheet.as_deref(),
1159                *row,
1160                *col,
1161                self.current_sheet,
1162            )?;
1163            return Ok(self.annotate_cell_value(sheet.as_deref(), *row, *col, value));
1164        }
1165
1166        let view = self
1167            .context
1168            .resolve_range_view(reference, self.current_sheet)?
1169            .with_cancel_token(self.context.cancellation_token());
1170        Ok(crate::traits::CalcValue::Range(view))
1171    }
1172
1173    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
1174        self.eval_reference_to_calc(reference)
1175            .map(|cv| cv.into_literal())
1176    }
1177
1178    /* ===================  unary ops  =================== */
1179    fn eval_unary(&self, op: &str, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
1180        if op == "@" {
1181            if let ASTNodeType::Reference { reference, .. } = &expr.node_type {
1182                let reference = self.effective_reference(reference)?;
1183                return Ok(self.implicit_intersection_from_reference(&reference));
1184            }
1185
1186            let cv = self.evaluate_ast(expr)?;
1187            return Ok(self.eval_implicit_intersection_calc(cv));
1188        }
1189
1190        let v = self.evaluate_ast(expr)?.into_literal();
1191        match v {
1192            LiteralValue::Array(arr) => {
1193                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
1194            }
1195            other => self.eval_unary_scalar(op, other),
1196        }
1197    }
1198
1199    fn eval_unary_scalar(&self, op: &str, v: LiteralValue) -> Result<LiteralValue, ExcelError> {
1200        match op {
1201            // Excel/LibreOffice treat unary `+` as a pass-through (identity) operator,
1202            // not as a numeric coercion. `=+"2014F"` returns the text "2014F"; only the
1203            // unary `-` form coerces operands to numbers. The `=+A1` idiom is common in
1204            // finance models (Lotus 1-2-3 carry-over) and must preserve text labels.
1205            "+" => Ok(v),
1206            "-" => self.apply_number_unary(v, b'-'),
1207            "%" => self.apply_number_unary(v, b'%'),
1208            _ => {
1209                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
1210            }
1211        }
1212    }
1213
1214    fn eval_implicit_intersection_calc(&self, cv: crate::traits::CalcValue<'a>) -> LiteralValue {
1215        let (cur_r0, cur_c0) = match self.current_cell {
1216            Some(cell) => (cell.coord.row() as usize, cell.coord.col() as usize),
1217            None => (0usize, 0usize),
1218        };
1219
1220        match cv {
1221            crate::traits::CalcValue::Scalar(v)
1222            | crate::traits::CalcValue::AnnotatedScalar(v, _) => match v {
1223                LiteralValue::Array(arr) => {
1224                    if arr.is_empty() || arr.first().map(|r| r.is_empty()).unwrap_or(true) {
1225                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1226                    }
1227                    arr[0][0].clone()
1228                }
1229                other => other,
1230            },
1231            crate::traits::CalcValue::Range(rv) => {
1232                if rv.is_empty() {
1233                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1234                }
1235
1236                // Array results (array literals and many dynamic-array functions) are materialized
1237                // into an owned RangeView with a temporary backing sheet ("__tmp").
1238                // For explicit @, interpret these as anchored at the formula cell and select the
1239                // top-left element.
1240                if rv.sheet_name() == "__tmp" {
1241                    return rv.get_cell(0, 0);
1242                }
1243
1244                if let Some(v) = rv.as_1x1() {
1245                    return v;
1246                }
1247
1248                let (rows, cols) = rv.dims();
1249                let sr = rv.start_row();
1250                let sc = rv.start_col();
1251                let er = rv.end_row();
1252                let ec = rv.end_col();
1253
1254                // Excel-compatible implicit intersection (simplified):
1255                // - Nx1: pick by row
1256                // - 1xM: pick by column
1257                // - NxM: pick by (row,col)
1258                if cols == 1 {
1259                    if cur_r0 < sr || cur_r0 > er {
1260                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1261                    }
1262                    let rel_r = cur_r0 - sr;
1263                    return rv.get_cell(rel_r, 0);
1264                }
1265
1266                if rows == 1 {
1267                    if cur_c0 < sc || cur_c0 > ec {
1268                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1269                    }
1270                    let rel_c = cur_c0 - sc;
1271                    return rv.get_cell(0, rel_c);
1272                }
1273
1274                if cur_r0 < sr || cur_r0 > er || cur_c0 < sc || cur_c0 > ec {
1275                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1276                }
1277                let rel_r = cur_r0 - sr;
1278                let rel_c = cur_c0 - sc;
1279                rv.get_cell(rel_r, rel_c)
1280            }
1281            crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
1282                ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
1283            ),
1284        }
1285    }
1286
1287    fn implicit_intersection_from_reference(&self, reference: &ReferenceType) -> LiteralValue {
1288        let (cur_r1, cur_c1) = match self.current_cell {
1289            Some(cell) => (
1290                cell.coord.row().saturating_add(1),
1291                cell.coord.col().saturating_add(1),
1292            ),
1293            None => (1u32, 1u32),
1294        };
1295
1296        match reference {
1297            ReferenceType::Cell {
1298                sheet, row, col, ..
1299            } => {
1300                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1301                match self
1302                    .context
1303                    .resolve_cell_reference(Some(sheet_name), *row, *col)
1304                {
1305                    Ok(v) => v,
1306                    Err(e) => LiteralValue::Error(e),
1307                }
1308            }
1309            ReferenceType::Range {
1310                sheet,
1311                start_row,
1312                start_col,
1313                end_row,
1314                end_col,
1315                ..
1316            } => {
1317                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1318
1319                // Open-ended references (`A:A`, `21:21`, `A5:A`) intersect by
1320                // their declared sheet bounds, not by a used-region view: a
1321                // whole row's intersection with column G is G on that row even
1322                // when the row's used cells end before G.
1323                const SHEET_ROWS: u32 = 1_048_576;
1324                const SHEET_COLS: u32 = 16_384;
1325                let (sr, sc, er, ec) = (
1326                    start_row.unwrap_or(1),
1327                    start_col.unwrap_or(1),
1328                    end_row.unwrap_or(SHEET_ROWS),
1329                    end_col.unwrap_or(SHEET_COLS),
1330                );
1331
1332                // Normalize bounds (A10:A1 is legal syntax; treat as swapped).
1333                let (mut sr, mut er) = (sr, er);
1334                let (mut sc, mut ec) = (sc, ec);
1335                if sr > er {
1336                    std::mem::swap(&mut sr, &mut er);
1337                }
1338                if sc > ec {
1339                    std::mem::swap(&mut sc, &mut ec);
1340                }
1341
1342                let pick = if sc == ec {
1343                    // Column vector: intersect by row
1344                    if cur_r1 < sr || cur_r1 > er {
1345                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1346                    }
1347                    (cur_r1, sc)
1348                } else if sr == er {
1349                    // Row vector: intersect by column
1350                    if cur_c1 < sc || cur_c1 > ec {
1351                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1352                    }
1353                    (sr, cur_c1)
1354                } else {
1355                    // 2D: require both axes
1356                    if cur_r1 < sr || cur_r1 > er || cur_c1 < sc || cur_c1 > ec {
1357                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1358                    }
1359                    (cur_r1, cur_c1)
1360                };
1361
1362                match self
1363                    .context
1364                    .resolve_cell_reference(Some(sheet_name), pick.0, pick.1)
1365                {
1366                    Ok(v) => v,
1367                    Err(e) => LiteralValue::Error(e),
1368                }
1369            }
1370            // Named ranges / tables / external: fall back to materializing and intersecting.
1371            other => {
1372                let cv = match self.eval_reference_to_calc(other) {
1373                    Ok(cv) => cv,
1374                    Err(e) => return LiteralValue::Error(e),
1375                };
1376                self.eval_implicit_intersection_calc(cv)
1377            }
1378        }
1379    }
1380
1381    fn apply_number_unary(&self, v: LiteralValue, op: u8) -> Result<LiteralValue, ExcelError> {
1382        match crate::coercion::to_arithmetic_number_with_locale(
1383            &v,
1384            &self.context.locale(),
1385            self.context.date_system(),
1386        ) {
1387            Ok(n) => match unary_f64(op, n) {
1388                Ok(n2) => Ok(LiteralValue::Number(n2)),
1389                Err(e) => Ok(LiteralValue::Error(e)),
1390            },
1391            Err(e) => Ok(LiteralValue::Error(e)),
1392        }
1393    }
1394
1395    /* ===================  binary ops  =================== */
1396    fn eval_binary(
1397        &self,
1398        op: &str,
1399        left_node: &ASTNode,
1400        right_node: &ASTNode,
1401    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1402        let left_calc = self.evaluate_ast(left_node)?;
1403        let left_format = left_calc.format_id();
1404        let left = left_calc.into_literal();
1405        let right_calc = self.evaluate_ast(right_node)?;
1406        let right_format = right_calc.format_id();
1407        let right = right_calc.into_literal();
1408        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
1409            return self
1410                .compare(op, left, right)
1411                .map(crate::traits::CalcValue::Scalar);
1412        }
1413        match op {
1414            "+" => self.numeric_binary(left, right, b'+').map(|value| {
1415                self.annotate_numeric_result(
1416                    value,
1417                    self.binary_format('+', left_format, right_format),
1418                )
1419            }),
1420            "-" => self.numeric_binary(left, right, b'-').map(|value| {
1421                self.annotate_numeric_result(
1422                    value,
1423                    self.binary_format('-', left_format, right_format),
1424                )
1425            }),
1426            "*" => self
1427                .numeric_binary(left, right, b'*')
1428                .map(crate::traits::CalcValue::Scalar),
1429            "/" => self
1430                .numeric_binary(left, right, b'/')
1431                .map(crate::traits::CalcValue::Scalar),
1432            "^" => self
1433                .numeric_binary(left, right, b'^')
1434                .map(crate::traits::CalcValue::Scalar),
1435            "&" => self
1436                .concat_values(left, right)
1437                .map(crate::traits::CalcValue::Scalar),
1438            ":" => {
1439                let left_ref = self.evaluate_ast_as_reference(left_node)?;
1440                let right_ref = self.evaluate_ast_as_reference(right_node)?;
1441                match crate::reference::combine_references(&left_ref, &right_ref) {
1442                    Ok(_) => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
1443                        "Reference produced by ':' cannot be used directly as a value",
1444                    )),
1445                    Err(error) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(error))),
1446                }
1447            }
1448            _ => {
1449                Err(ExcelError::new(ExcelErrorKind::NImpl)
1450                    .with_message(format!("Binary op '{op}'")))
1451            }
1452        }
1453    }
1454
1455    /* ===================  function calls  =================== */
1456    fn eval_function_to_calc(
1457        &self,
1458        name: &str,
1459        args: &[ASTNode],
1460    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1461        if let Some(fun) = self.context.get_function("", name) {
1462            let handles: Vec<ArgumentHandle> =
1463                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
1464            // Use the function's built-in dispatch method with a narrow FunctionContext
1465            let fctx = DefaultFunctionContext::new_with_sheet(
1466                self.context,
1467                self.current_cell,
1468                self.current_sheet,
1469            );
1470            return fun.dispatch(&handles, &fctx);
1471        }
1472
1473        if let Some(callable) = self.resolve_local_callable(name) {
1474            let mut eval_args = Vec::with_capacity(args.len());
1475            let mut arg_references = Vec::with_capacity(args.len());
1476            for arg in args {
1477                // Same rule `LET` uses to keep a bound range: a
1478                // syntactic reference expression, or a local that itself
1479                // carries a bound reference; nothing else.
1480                arg_references
1481                    .push(ArgumentHandle::new(arg, self).bound_reference_in_env(&self.local_env));
1482                eval_args.push(self.evaluate_ast(arg)?.into_literal());
1483            }
1484            return callable.invoke_with_references(self, &eval_args, &arg_references);
1485        }
1486
1487        // Include the function name in the error message for better debugging
1488        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1489            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown function: {name}")),
1490        )))
1491    }
1492
1493    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
1494        self.eval_function_to_calc(name, args)
1495            .map(|cv| cv.into_literal())
1496    }
1497
1498    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
1499        DefaultFunctionContext::new_with_sheet(self.context, cell_ref.cloned(), self.current_sheet)
1500    }
1501
1502    /* ===================  array literal  =================== */
1503    fn eval_array_literal_to_calc(
1504        &self,
1505        rows: &[Vec<ASTNode>],
1506    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1507        let mut out = Vec::with_capacity(rows.len());
1508        for row in rows {
1509            let mut r = Vec::with_capacity(row.len());
1510            for cell in row {
1511                r.push(self.evaluate_ast(cell)?.into_literal());
1512            }
1513            out.push(r);
1514        }
1515        Ok(crate::traits::CalcValue::Range(
1516            crate::engine::range_view::RangeView::from_owned_rows(out, self.context.date_system()),
1517        ))
1518    }
1519
1520    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
1521        self.eval_array_literal_to_calc(rows)
1522            .map(|cv| cv.into_literal())
1523    }
1524
1525    /// `+ - * / ^` on two operands: arrays broadcast, each element pair is
1526    /// coerced to numbers (first error wins, left first) and combined by
1527    /// [`arith_f64`].
1528    fn numeric_binary(
1529        &self,
1530        left: LiteralValue,
1531        right: LiteralValue,
1532        op: u8,
1533    ) -> Result<LiteralValue, ExcelError> {
1534        self.broadcast_apply(left, right, |l, r| {
1535            let a = crate::coercion::to_arithmetic_number_with_locale(
1536                &l,
1537                &self.context.locale(),
1538                self.context.date_system(),
1539            );
1540            let b = crate::coercion::to_arithmetic_number_with_locale(
1541                &r,
1542                &self.context.locale(),
1543                self.context.date_system(),
1544            );
1545            match (a, b) {
1546                (Ok(a), Ok(b)) => match arith_f64(op, a, b) {
1547                    Ok(n) => Ok(LiteralValue::Number(n)),
1548                    Err(e) => Ok(LiteralValue::Error(e)),
1549                },
1550                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
1551            }
1552        })
1553    }
1554
1555    /// The `&` operator. Like the arithmetic operators it works element by
1556    /// element over arrays and ranges, and an error operand is the result (the
1557    /// left one when both are errors) instead of being spelled into the text.
1558    fn concat_values(
1559        &self,
1560        left: LiteralValue,
1561        right: LiteralValue,
1562    ) -> Result<LiteralValue, ExcelError> {
1563        fn concat_scalar(
1564            left: LiteralValue,
1565            right: LiteralValue,
1566        ) -> Result<LiteralValue, ExcelError> {
1567            Ok(match (left, right) {
1568                (LiteralValue::Error(error), _) | (_, LiteralValue::Error(error)) => {
1569                    LiteralValue::Error(error)
1570                }
1571                (left, right) => LiteralValue::Text(format!(
1572                    "{}{}",
1573                    crate::coercion::to_text_invariant(&left),
1574                    crate::coercion::to_text_invariant(&right)
1575                )),
1576            })
1577        }
1578        // Scalars (every member of a lifted family) skip the broadcast.
1579        if matches!(left, LiteralValue::Array(_)) || matches!(right, LiteralValue::Array(_)) {
1580            self.broadcast_apply(left, right, concat_scalar)
1581        } else {
1582            concat_scalar(left, right)
1583        }
1584    }
1585
1586    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
1587    where
1588        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1589    {
1590        let mut out = Vec::with_capacity(arr.len());
1591        for row in arr {
1592            let mut new_row = Vec::with_capacity(row.len());
1593            for cell in row {
1594                new_row.push(match f(cell) {
1595                    Ok(v) => v,
1596                    Err(e) => LiteralValue::Error(e),
1597                });
1598            }
1599            out.push(new_row);
1600        }
1601        Ok(LiteralValue::Array(out))
1602    }
1603
1604    fn combine_arrays<F>(
1605        &self,
1606        l: Vec<Vec<LiteralValue>>,
1607        r: Vec<Vec<LiteralValue>>,
1608        f: F,
1609    ) -> Result<LiteralValue, ExcelError>
1610    where
1611        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1612    {
1613        // Use strict broadcasting across dimensions
1614        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
1615        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
1616        let target = match broadcast_shape(&[l_shape, r_shape]) {
1617            Ok(s) => s,
1618            Err(e) => return Ok(LiteralValue::Error(e)),
1619        };
1620
1621        let mut out = Vec::with_capacity(target.0);
1622        for i in 0..target.0 {
1623            let mut row = Vec::with_capacity(target.1);
1624            for j in 0..target.1 {
1625                let (li, lj) = project_index((i, j), l_shape);
1626                let (ri, rj) = project_index((i, j), r_shape);
1627                let lv = l
1628                    .get(li)
1629                    .and_then(|r| r.get(lj))
1630                    .cloned()
1631                    .unwrap_or(LiteralValue::Empty);
1632                let rv = r
1633                    .get(ri)
1634                    .and_then(|r| r.get(rj))
1635                    .cloned()
1636                    .unwrap_or(LiteralValue::Empty);
1637                row.push(match f(lv, rv) {
1638                    Ok(v) => v,
1639                    Err(e) => LiteralValue::Error(e),
1640                });
1641            }
1642            out.push(row);
1643        }
1644        Ok(LiteralValue::Array(out))
1645    }
1646
1647    fn broadcast_apply<F>(
1648        &self,
1649        left: LiteralValue,
1650        right: LiteralValue,
1651        f: F,
1652    ) -> Result<LiteralValue, ExcelError>
1653    where
1654        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1655    {
1656        use LiteralValue::*;
1657        match (left, right) {
1658            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
1659            (Array(arr), v) => {
1660                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1661                let shape_r = (1usize, 1usize);
1662                let target = match broadcast_shape(&[shape_l, shape_r]) {
1663                    Ok(s) => s,
1664                    Err(e) => return Ok(LiteralValue::Error(e)),
1665                };
1666                let mut out = Vec::with_capacity(target.0);
1667                for i in 0..target.0 {
1668                    let mut row = Vec::with_capacity(target.1);
1669                    for j in 0..target.1 {
1670                        let (li, lj) = project_index((i, j), shape_l);
1671                        let lv = arr
1672                            .get(li)
1673                            .and_then(|r| r.get(lj))
1674                            .cloned()
1675                            .unwrap_or(LiteralValue::Empty);
1676                        row.push(match f(lv, v.clone()) {
1677                            Ok(vv) => vv,
1678                            Err(e) => LiteralValue::Error(e),
1679                        });
1680                    }
1681                    out.push(row);
1682                }
1683                Ok(LiteralValue::Array(out))
1684            }
1685            (v, Array(arr)) => {
1686                let shape_l = (1usize, 1usize);
1687                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1688                let target = match broadcast_shape(&[shape_l, shape_r]) {
1689                    Ok(s) => s,
1690                    Err(e) => return Ok(LiteralValue::Error(e)),
1691                };
1692                let mut out = Vec::with_capacity(target.0);
1693                for i in 0..target.0 {
1694                    let mut row = Vec::with_capacity(target.1);
1695                    for j in 0..target.1 {
1696                        let (ri, rj) = project_index((i, j), shape_r);
1697                        let rv = arr
1698                            .get(ri)
1699                            .and_then(|r| r.get(rj))
1700                            .cloned()
1701                            .unwrap_or(LiteralValue::Empty);
1702                        row.push(match f(v.clone(), rv) {
1703                            Ok(vv) => vv,
1704                            Err(e) => LiteralValue::Error(e),
1705                        });
1706                    }
1707                    out.push(row);
1708                }
1709                Ok(LiteralValue::Array(out))
1710            }
1711            (l, r) => f(l, r),
1712        }
1713    }
1714
1715    /* ---------- coercion helpers ---------- */
1716    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
1717        coercion::to_number_lenient(v)
1718    }
1719
1720    fn coerce_text(&self, v: &LiteralValue) -> String {
1721        coercion::to_text_invariant(v)
1722    }
1723
1724    /* ---------- comparison ---------- */
1725    fn compare(
1726        &self,
1727        op: &str,
1728        left: LiteralValue,
1729        right: LiteralValue,
1730    ) -> Result<LiteralValue, ExcelError> {
1731        use LiteralValue::*;
1732        if matches!(left, Error(_)) {
1733            return Ok(left);
1734        }
1735        if matches!(right, Error(_)) {
1736            return Ok(right);
1737        }
1738
1739        // arrays: element‑wise with broadcasting
1740        match (left, right) {
1741            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
1742            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
1743            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
1744            (l, r) => {
1745                let res = match (l, r) {
1746                    // Same-rank fast paths. These agree with `cmp_ranked` below
1747                    // and exist only to avoid the rank/`Empty` dance.
1748                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
1749                    (Int(a), Int(b)) => self.cmp_f64(a as f64, b as f64, op),
1750                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
1751                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
1752                    (Boolean(a), Boolean(b)) => {
1753                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
1754                    }
1755                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
1756                    (a, b) => self.cmp_ranked(a, b, op),
1757                };
1758                Ok(LiteralValue::Boolean(res))
1759            }
1760        }
1761    }
1762
1763    /// Mixed-type relational comparison.
1764    ///
1765    /// Excel does not coerce across types in `<`, `<=`, `>`, `>=`, `=`, `<>`.
1766    /// It applies a pure type rank, identical for all six operators:
1767    ///
1768    /// ```text
1769    /// number  <  text  <  boolean
1770    /// ```
1771    ///
1772    /// Only when both operands share a rank are they compared within the type
1773    /// (measured in Excel for Mac 16.105.3, en_US). Consequences of the rank: `TRUE=1` is FALSE, `1<TRUE` is TRUE,
1774    /// `"5"=5` is FALSE (numeric text never coerces), `"5">4` is TRUE,
1775    /// `"TRUE"=TRUE` is FALSE, and `"Z"<FALSE` is TRUE.
1776    ///
1777    /// Errors never reach here: `compare` short-circuits them above.
1778    fn cmp_ranked(&self, l: LiteralValue, r: LiteralValue, op: &str) -> bool {
1779        use LiteralValue::*;
1780
1781        // A never-written cell is polymorphic rather than ranked: it adopts the
1782        // other operand's type and behaves as that type's zero value, so
1783        // `Z1=0`, `Z1=""`, `Z1=FALSE` and `Z1<TRUE` are all TRUE.
1784        let (l, r) = match (l, r) {
1785            (Empty, Empty) => (Number(0.0), Number(0.0)),
1786            (Empty, r) => (empty_as_zero_of(&r), r),
1787            (l, Empty) => {
1788                let e = empty_as_zero_of(&l);
1789                (l, e)
1790            }
1791            (l, r) => (l, r),
1792        };
1793
1794        let (lr, rr) = (excel_type_rank(&l), excel_type_rank(&r));
1795        if lr != rr {
1796            return self.cmp_f64(lr as f64, rr as f64, op);
1797        }
1798
1799        match (l, r) {
1800            (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
1801            (Boolean(a), Boolean(b)) => {
1802                self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
1803            }
1804            // Same rank, number class: Int/Number and the date/time/duration
1805            // serial-bearing variants. Text is never parsed here.
1806            (a, b) => {
1807                let an = crate::coercion::to_number_strict(&a).ok();
1808                let bn = crate::coercion::to_number_strict(&b).ok();
1809                if let (Some(a), Some(b)) = (an, bn) {
1810                    self.cmp_f64(a, b, op)
1811                } else {
1812                    // Only `Pending` (and any future non-numeric, non-text,
1813                    // non-boolean variant) lands here; keep the legacy text
1814                    // fallback so those pairs behave as before.
1815                    self.cmp_text(
1816                        &crate::coercion::to_text_invariant(&a),
1817                        &crate::coercion::to_text_invariant(&b),
1818                        op,
1819                    )
1820                }
1821            }
1822        }
1823    }
1824
1825    fn cmp_f64(&self, a: f64, b: f64, op: &str) -> bool {
1826        cmp_f64(a, b, op)
1827    }
1828    fn cmp_text(&self, a: &str, b: &str, op: &str) -> bool {
1829        let loc = self.context.locale();
1830        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
1831        self.cmp_f64(
1832            a.cmp(&b) as i32 as f64,
1833            0.0,
1834            match op {
1835                "=" => "=",
1836                "<>" => "<>",
1837                ">" => ">",
1838                "<" => "<",
1839                ">=" => ">=",
1840                "<=" => "<=",
1841                _ => unreachable!(),
1842            },
1843        )
1844    }
1845}
1846
1847/// A numeric comparison operator on two numbers (shared by the walk and
1848/// the typed lift).
1849#[inline]
1850pub(crate) fn cmp_f64(a: f64, b: f64, op: &str) -> bool {
1851    match op {
1852        "=" => a == b,
1853        "<>" => a != b,
1854        ">" => a > b,
1855        "<" => a < b,
1856        ">=" => a >= b,
1857        "<=" => a <= b,
1858        _ => unreachable!(),
1859    }
1860}
1861
1862/// `+ - * / ^` on two numbers after coercion: the result, or the error
1863/// the operator yields (`#DIV/0!`, `#NUM!` for a negative base with a
1864/// fractional exponent or a non-finite result). Shared by the walk and
1865/// the typed lift, so both are bit-identical.
1866#[inline]
1867pub(crate) fn arith_f64(op: u8, a: f64, b: f64) -> Result<f64, ExcelError> {
1868    let v = match op {
1869        b'+' => a + b,
1870        b'-' => a - b,
1871        b'*' => a * b,
1872        b'/' => {
1873            if b == 0.0 {
1874                return Err(ExcelError::from_error_string("#DIV/0!"));
1875            }
1876            a / b
1877        }
1878        b'^' => {
1879            // Excel domain: negative base with non-integer exponent -> #NUM!
1880            if a < 0.0 && b.fract() != 0.0 {
1881                return Err(ExcelError::new_num());
1882            }
1883            a.powf(b)
1884        }
1885        _ => unreachable!("arithmetic operator"),
1886    };
1887    crate::coercion::sanitize_numeric(v)
1888}
1889
1890/// Unary `-` and `%` on a number after coercion (shared by the walk and
1891/// the typed lift).
1892#[inline]
1893pub(crate) fn unary_f64(op: u8, n: f64) -> Result<f64, ExcelError> {
1894    crate::coercion::sanitize_numeric(match op {
1895        b'-' => -n,
1896        b'%' => n / 100.0,
1897        _ => unreachable!("unary operator"),
1898    })
1899}
1900
1901/// Excel's relational type rank: `number < text < boolean`.
1902///
1903/// The number class covers `Int`, `Number` and the serial-bearing temporal
1904/// variants (`Date`, `DateTime`, `Time`, `Duration`), because on a sheet a date
1905/// cell *is* a number. `Empty` is deliberately not ranked here — it is
1906/// polymorphic and must be resolved against the other operand before ranking
1907/// (see `Interpreter::cmp_ranked`). `Error` never reaches ranking.
1908fn excel_type_rank(v: &LiteralValue) -> u8 {
1909    match v {
1910        LiteralValue::Text(_) => 1,
1911        LiteralValue::Boolean(_) => 2,
1912        _ => 0,
1913    }
1914}
1915
1916/// The zero value of `other`'s type, used to give a blank operand the type of
1917/// whatever it is compared against: `0` against a number, `""` against text,
1918/// `FALSE` against a boolean.
1919fn empty_as_zero_of(other: &LiteralValue) -> LiteralValue {
1920    match other {
1921        LiteralValue::Text(_) => LiteralValue::Text(String::new()),
1922        LiteralValue::Boolean(_) => LiteralValue::Boolean(false),
1923        _ => LiteralValue::Number(0.0),
1924    }
1925}
1926
1927fn relocate_reference_for_offset(
1928    reference: &ReferenceType,
1929    row_delta: i64,
1930    col_delta: i64,
1931) -> Result<ReferenceType, ExcelError> {
1932    match reference {
1933        ReferenceType::Cell {
1934            sheet,
1935            row,
1936            col,
1937            row_abs,
1938            col_abs,
1939        } => Ok(ReferenceType::Cell {
1940            sheet: sheet.clone(),
1941            row: shift_axis_for_offset(*row, row_delta, *row_abs)?,
1942            col: shift_axis_for_offset(*col, col_delta, *col_abs)?,
1943            row_abs: *row_abs,
1944            col_abs: *col_abs,
1945        }),
1946        ReferenceType::Range {
1947            sheet,
1948            start_row,
1949            start_col,
1950            end_row,
1951            end_col,
1952            start_row_abs,
1953            start_col_abs,
1954            end_row_abs,
1955            end_col_abs,
1956        } => Ok(ReferenceType::Range {
1957            sheet: sheet.clone(),
1958            start_row: shift_optional_axis_for_offset(*start_row, row_delta, *start_row_abs)?,
1959            start_col: shift_optional_axis_for_offset(*start_col, col_delta, *start_col_abs)?,
1960            end_row: shift_optional_axis_for_offset(*end_row, row_delta, *end_row_abs)?,
1961            end_col: shift_optional_axis_for_offset(*end_col, col_delta, *end_col_abs)?,
1962            start_row_abs: *start_row_abs,
1963            start_col_abs: *start_col_abs,
1964            end_row_abs: *end_row_abs,
1965            end_col_abs: *end_col_abs,
1966        }),
1967        // Defined names are placement-invariant: a relocated copy of the
1968        // formula references the same name, resolved at evaluation time.
1969        ReferenceType::NamedRange(name) => Ok(ReferenceType::NamedRange(name.clone())),
1970        ReferenceType::Table(_)
1971        | ReferenceType::Cell3D { .. }
1972        | ReferenceType::Range3D { .. }
1973        | ReferenceType::External(_) => Err(unsupported_reference_relocation_error()),
1974    }
1975}
1976
1977fn shift_optional_axis_for_offset(
1978    value: Option<u32>,
1979    delta: i64,
1980    is_absolute: bool,
1981) -> Result<Option<u32>, ExcelError> {
1982    value
1983        .map(|value| shift_axis_for_offset(value, delta, is_absolute))
1984        .transpose()
1985}
1986
1987pub(crate) fn shift_axis_for_offset(
1988    value: u32,
1989    delta: i64,
1990    is_absolute: bool,
1991) -> Result<u32, ExcelError> {
1992    if is_absolute {
1993        return Ok(value);
1994    }
1995    let shifted = i64::from(value) + delta;
1996    if shifted < 1 || shifted > i64::from(u32::MAX) {
1997        return Err(unsupported_reference_relocation_error());
1998    }
1999    Ok(shifted as u32)
2000}
2001
2002fn unsupported_reference_relocation_error() -> ExcelError {
2003    ExcelError::new(ExcelErrorKind::Ref)
2004        .with_message("Unsupported reference relocation for FormulaPlane span evaluation")
2005}
2006
2007#[cfg(test)]
2008mod format_algebra_tests {
2009    use super::*;
2010    use crate::engine::{EvalConfig, eval::Engine};
2011    use crate::format::FormatId;
2012    use crate::test_workbook::TestWorkbook;
2013
2014    #[test]
2015    fn temporal_binary_format_algebra_pins_positive_and_negative_cases() {
2016        let engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
2017        let interpreter = Interpreter::new(&engine, "Sheet1");
2018
2019        assert_eq!(
2020            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId::TIME)),
2021            Some(FormatId::DATETIME)
2022        );
2023        assert_eq!(
2024            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId(9))),
2025            Some(FormatId::DATE),
2026            "Date + Percent follows the measured temporal-wins rule"
2027        );
2028        assert_eq!(
2029            interpreter.binary_format('-', Some(FormatId::DATE), Some(FormatId::DATE)),
2030            None,
2031            "Date - Date is an unformatted duration in days"
2032        );
2033        assert_eq!(
2034            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId(49))),
2035            None,
2036            "Date + Text must not acquire a temporal annotation"
2037        );
2038        for (left, right) in [
2039            (FormatId::DATE, FormatId::DATE),
2040            (FormatId::DURATION, FormatId::DATE),
2041            (FormatId::DATE, FormatId(5)),
2042            (FormatId::DATETIME, FormatId::TIME),
2043        ] {
2044            assert_eq!(
2045                interpreter.binary_format('+', Some(left), Some(right)),
2046                None
2047            );
2048        }
2049    }
2050}