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