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